2005-04-17 06:20:36 +08:00
|
|
|
/*
|
|
|
|
* Sound driver for Silicon Graphics 320 and 540 Visual Workstations'
|
|
|
|
* onboard audio. See notes in Documentation/sound/oss/vwsnd .
|
|
|
|
*
|
|
|
|
* Copyright 1999 Silicon Graphics, Inc. All rights reserved.
|
|
|
|
*
|
|
|
|
* 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., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#undef VWSND_DEBUG /* define for debugging */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* XXX to do -
|
|
|
|
*
|
|
|
|
* External sync.
|
|
|
|
* Rename swbuf, hwbuf, u&i, hwptr&swptr to something rational.
|
|
|
|
* Bug - if select() called before read(), pcm_setup() not called.
|
|
|
|
* Bug - output doesn't stop soon enough if process killed.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Things to test -
|
|
|
|
*
|
|
|
|
* Will readv/writev work? Write a test.
|
|
|
|
*
|
|
|
|
* insmod/rmmod 100 million times.
|
|
|
|
*
|
|
|
|
* Run I/O until int ptrs wrap around (roughly 6.2 hours @ DAT
|
|
|
|
* rate).
|
|
|
|
*
|
|
|
|
* Concurrent threads banging on mixer simultaneously, both UP
|
|
|
|
* and SMP kernels. Especially, watch for thread A changing
|
|
|
|
* OUTSRC while thread B changes gain -- both write to the same
|
|
|
|
* ad1843 register.
|
|
|
|
*
|
|
|
|
* What happens if a client opens /dev/audio then forks?
|
|
|
|
* Do two procs have /dev/audio open? Test.
|
|
|
|
*
|
|
|
|
* Pump audio through the CD, MIC and line inputs and verify that
|
|
|
|
* they mix/mute into the output.
|
|
|
|
*
|
|
|
|
* Apps:
|
|
|
|
* amp
|
|
|
|
* mpg123
|
|
|
|
* x11amp
|
|
|
|
* mxv
|
|
|
|
* kmedia
|
|
|
|
* esound
|
|
|
|
* need more input apps
|
|
|
|
*
|
|
|
|
* Run tests while bombarding with signals. setitimer(2) will do it... */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This driver is organized in nine sections.
|
|
|
|
* The nine sections are:
|
|
|
|
*
|
|
|
|
* debug stuff
|
|
|
|
* low level lithium access
|
|
|
|
* high level lithium access
|
|
|
|
* AD1843 access
|
|
|
|
* PCM I/O
|
|
|
|
* audio driver
|
|
|
|
* mixer driver
|
|
|
|
* probe/attach/unload
|
|
|
|
* initialization and loadable kernel module interface
|
|
|
|
*
|
|
|
|
* That is roughly the order of increasing abstraction, so forward
|
|
|
|
* dependencies are minimal.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Locking Notes
|
|
|
|
*
|
|
|
|
* INC_USE_COUNT and DEC_USE_COUNT keep track of the number of
|
|
|
|
* open descriptors to this driver. They store it in vwsnd_use_count.
|
|
|
|
* The global device list, vwsnd_dev_list, is immutable when the IN_USE
|
|
|
|
* is true.
|
|
|
|
*
|
|
|
|
* devc->open_lock is a semaphore that is used to enforce the
|
|
|
|
* single reader/single writer rule for /dev/audio. The rule is
|
|
|
|
* that each device may have at most one reader and one writer.
|
|
|
|
* Open will block until the previous client has closed the
|
|
|
|
* device, unless O_NONBLOCK is specified.
|
|
|
|
*
|
2006-03-23 19:00:39 +08:00
|
|
|
* The semaphore devc->io_mutex serializes PCM I/O syscalls. This
|
2005-04-17 06:20:36 +08:00
|
|
|
* is unnecessary in Linux 2.2, because the kernel lock
|
|
|
|
* serializes read, write, and ioctl globally, but it's there,
|
|
|
|
* ready for the brave, new post-kernel-lock world.
|
|
|
|
*
|
|
|
|
* Locking between interrupt and baselevel is handled by the
|
|
|
|
* "lock" spinlock in vwsnd_port (one lock each for read and
|
|
|
|
* write). Each half holds the lock just long enough to see what
|
|
|
|
* area it owns and update its pointers. See pcm_output() and
|
|
|
|
* pcm_input() for most of the gory stuff.
|
|
|
|
*
|
2006-03-23 19:00:39 +08:00
|
|
|
* devc->mix_mutex serializes all mixer ioctls. This is also
|
2005-04-17 06:20:36 +08:00
|
|
|
* redundant because of the kernel lock.
|
|
|
|
*
|
|
|
|
* The lowest level lock is lith->lithium_lock. It is a
|
|
|
|
* spinlock which is held during the two-register tango of
|
|
|
|
* reading/writing an AD1843 register. See
|
|
|
|
* li_{read,write}_ad1843_reg().
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Sample Format Notes
|
|
|
|
*
|
|
|
|
* Lithium's DMA engine has two formats: 16-bit 2's complement
|
|
|
|
* and 8-bit unsigned . 16-bit transfers the data unmodified, 2
|
|
|
|
* bytes per sample. 8-bit unsigned transfers 1 byte per sample
|
|
|
|
* and XORs each byte with 0x80. Lithium can input or output
|
|
|
|
* either mono or stereo in either format.
|
|
|
|
*
|
|
|
|
* The AD1843 has four formats: 16-bit 2's complement, 8-bit
|
|
|
|
* unsigned, 8-bit mu-Law and 8-bit A-Law.
|
|
|
|
*
|
|
|
|
* This driver supports five formats: AFMT_S8, AFMT_U8,
|
|
|
|
* AFMT_MU_LAW, AFMT_A_LAW, and AFMT_S16_LE.
|
|
|
|
*
|
|
|
|
* For AFMT_U8 output, we keep the AD1843 in 16-bit mode, and
|
|
|
|
* rely on Lithium's XOR to translate between U8 and S8.
|
|
|
|
*
|
|
|
|
* For AFMT_S8, AFMT_MU_LAW and AFMT_A_LAW output, we have to XOR
|
|
|
|
* the 0x80 bit in software to compensate for Lithium's XOR.
|
|
|
|
* This happens in pcm_copy_{in,out}().
|
|
|
|
*
|
|
|
|
* Changes:
|
|
|
|
* 11-10-2000 Bartlomiej Zolnierkiewicz <bkz@linux-ide.org>
|
|
|
|
* Added some __init/__exit
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <linux/module.h>
|
|
|
|
#include <linux/init.h>
|
|
|
|
|
|
|
|
#include <linux/spinlock.h>
|
|
|
|
#include <linux/smp_lock.h>
|
|
|
|
#include <linux/wait.h>
|
|
|
|
#include <linux/interrupt.h>
|
2006-03-23 19:00:39 +08:00
|
|
|
#include <linux/mutex.h>
|
include cleanup: Update gfp.h and slab.h includes to prepare for breaking implicit slab.h inclusion from percpu.h
percpu.h is included by sched.h and module.h and thus ends up being
included when building most .c files. percpu.h includes slab.h which
in turn includes gfp.h making everything defined by the two files
universally available and complicating inclusion dependencies.
percpu.h -> slab.h dependency is about to be removed. Prepare for
this change by updating users of gfp and slab facilities include those
headers directly instead of assuming availability. As this conversion
needs to touch large number of source files, the following script is
used as the basis of conversion.
http://userweb.kernel.org/~tj/misc/slabh-sweep.py
The script does the followings.
* Scan files for gfp and slab usages and update includes such that
only the necessary includes are there. ie. if only gfp is used,
gfp.h, if slab is used, slab.h.
* When the script inserts a new include, it looks at the include
blocks and try to put the new include such that its order conforms
to its surrounding. It's put in the include block which contains
core kernel includes, in the same order that the rest are ordered -
alphabetical, Christmas tree, rev-Xmas-tree or at the end if there
doesn't seem to be any matching order.
* If the script can't find a place to put a new include (mostly
because the file doesn't have fitting include block), it prints out
an error message indicating which .h file needs to be added to the
file.
The conversion was done in the following steps.
1. The initial automatic conversion of all .c files updated slightly
over 4000 files, deleting around 700 includes and adding ~480 gfp.h
and ~3000 slab.h inclusions. The script emitted errors for ~400
files.
2. Each error was manually checked. Some didn't need the inclusion,
some needed manual addition while adding it to implementation .h or
embedding .c file was more appropriate for others. This step added
inclusions to around 150 files.
3. The script was run again and the output was compared to the edits
from #2 to make sure no file was left behind.
4. Several build tests were done and a couple of problems were fixed.
e.g. lib/decompress_*.c used malloc/free() wrappers around slab
APIs requiring slab.h to be added manually.
5. The script was run on all .h files but without automatically
editing them as sprinkling gfp.h and slab.h inclusions around .h
files could easily lead to inclusion dependency hell. Most gfp.h
inclusion directives were ignored as stuff from gfp.h was usually
wildly available and often used in preprocessor macros. Each
slab.h inclusion directive was examined and added manually as
necessary.
6. percpu.h was updated not to include slab.h.
7. Build test were done on the following configurations and failures
were fixed. CONFIG_GCOV_KERNEL was turned off for all tests (as my
distributed build env didn't work with gcov compiles) and a few
more options had to be turned off depending on archs to make things
build (like ipr on powerpc/64 which failed due to missing writeq).
* x86 and x86_64 UP and SMP allmodconfig and a custom test config.
* powerpc and powerpc64 SMP allmodconfig
* sparc and sparc64 SMP allmodconfig
* ia64 SMP allmodconfig
* s390 SMP allmodconfig
* alpha SMP allmodconfig
* um on x86_64 SMP allmodconfig
8. percpu.h modifications were reverted so that it could be applied as
a separate patch and serve as bisection point.
Given the fact that I had only a couple of failures from tests on step
6, I'm fairly confident about the coverage of this conversion patch.
If there is a breakage, it's likely to be something in one of the arch
headers which should be easily discoverable easily on most builds of
the specific arch.
Signed-off-by: Tejun Heo <tj@kernel.org>
Guess-its-ok-by: Christoph Lameter <cl@linux-foundation.org>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Lee Schermerhorn <Lee.Schermerhorn@hp.com>
2010-03-24 16:04:11 +08:00
|
|
|
#include <linux/slab.h>
|
2006-03-23 19:00:39 +08:00
|
|
|
|
2008-07-11 01:35:33 +08:00
|
|
|
#include <asm/visws/cobalt.h>
|
2005-04-17 06:20:36 +08:00
|
|
|
|
|
|
|
#include "sound_config.h"
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* debug stuff */
|
|
|
|
|
|
|
|
#ifdef VWSND_DEBUG
|
|
|
|
|
|
|
|
static int shut_up = 1;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* dbgassert - called when an assertion fails.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void dbgassert(const char *fcn, int line, const char *expr)
|
|
|
|
{
|
|
|
|
if (in_interrupt())
|
|
|
|
panic("ASSERTION FAILED IN INTERRUPT, %s:%s:%d %s\n",
|
|
|
|
__FILE__, fcn, line, expr);
|
|
|
|
else {
|
|
|
|
int x;
|
|
|
|
printk(KERN_ERR "ASSERTION FAILED, %s:%s:%d %s\n",
|
|
|
|
__FILE__, fcn, line, expr);
|
|
|
|
x = * (volatile int *) 0; /* force proc to exit */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Bunch of useful debug macros:
|
|
|
|
*
|
|
|
|
* ASSERT - print unless e nonzero (panic if in interrupt)
|
|
|
|
* DBGDO - include arbitrary code if debugging
|
|
|
|
* DBGX - debug print raw (w/o function name)
|
|
|
|
* DBGP - debug print w/ function name
|
|
|
|
* DBGE - debug print function entry
|
|
|
|
* DBGC - debug print function call
|
|
|
|
* DBGR - debug print function return
|
|
|
|
* DBGXV - debug print raw when verbose
|
|
|
|
* DBGPV - debug print when verbose
|
|
|
|
* DBGEV - debug print function entry when verbose
|
|
|
|
* DBGRV - debug print function return when verbose
|
|
|
|
*/
|
|
|
|
|
2008-03-04 07:32:18 +08:00
|
|
|
#define ASSERT(e) ((e) ? (void) 0 : dbgassert(__func__, __LINE__, #e))
|
2005-04-17 06:20:36 +08:00
|
|
|
#define DBGDO(x) x
|
|
|
|
#define DBGX(fmt, args...) (in_interrupt() ? 0 : printk(KERN_ERR fmt, ##args))
|
2008-03-04 07:32:18 +08:00
|
|
|
#define DBGP(fmt, args...) (DBGX("%s: " fmt, __func__ , ##args))
|
|
|
|
#define DBGE(fmt, args...) (DBGX("%s" fmt, __func__ , ##args))
|
2005-04-17 06:20:36 +08:00
|
|
|
#define DBGC(rtn) (DBGP("calling %s\n", rtn))
|
|
|
|
#define DBGR() (DBGP("returning\n"))
|
|
|
|
#define DBGXV(fmt, args...) (shut_up ? 0 : DBGX(fmt, ##args))
|
|
|
|
#define DBGPV(fmt, args...) (shut_up ? 0 : DBGP(fmt, ##args))
|
|
|
|
#define DBGEV(fmt, args...) (shut_up ? 0 : DBGE(fmt, ##args))
|
|
|
|
#define DBGCV(rtn) (shut_up ? 0 : DBGC(rtn))
|
|
|
|
#define DBGRV() (shut_up ? 0 : DBGR())
|
|
|
|
|
|
|
|
#else /* !VWSND_DEBUG */
|
|
|
|
|
|
|
|
#define ASSERT(e) ((void) 0)
|
|
|
|
#define DBGDO(x) /* don't */
|
|
|
|
#define DBGX(fmt, args...) ((void) 0)
|
|
|
|
#define DBGP(fmt, args...) ((void) 0)
|
|
|
|
#define DBGE(fmt, args...) ((void) 0)
|
|
|
|
#define DBGC(rtn) ((void) 0)
|
|
|
|
#define DBGR() ((void) 0)
|
|
|
|
#define DBGPV(fmt, args...) ((void) 0)
|
|
|
|
#define DBGXV(fmt, args...) ((void) 0)
|
|
|
|
#define DBGEV(fmt, args...) ((void) 0)
|
|
|
|
#define DBGCV(rtn) ((void) 0)
|
|
|
|
#define DBGRV() ((void) 0)
|
|
|
|
|
|
|
|
#endif /* !VWSND_DEBUG */
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* low level lithium access */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We need to talk to Lithium registers on three pages. Here are
|
|
|
|
* the pages' offsets from the base address (0xFF001000).
|
|
|
|
*/
|
|
|
|
|
|
|
|
enum {
|
|
|
|
LI_PAGE0_OFFSET = 0x01000 - 0x1000, /* FF001000 */
|
|
|
|
LI_PAGE1_OFFSET = 0x0F000 - 0x1000, /* FF00F000 */
|
|
|
|
LI_PAGE2_OFFSET = 0x10000 - 0x1000, /* FF010000 */
|
|
|
|
};
|
|
|
|
|
|
|
|
/* low-level lithium data */
|
|
|
|
|
|
|
|
typedef struct lithium {
|
|
|
|
void * page0; /* virtual addresses */
|
|
|
|
void * page1;
|
|
|
|
void * page2;
|
|
|
|
spinlock_t lock; /* protects codec and UST/MSC access */
|
|
|
|
} lithium_t;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* li_destroy destroys the lithium_t structure and vm mappings.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void li_destroy(lithium_t *lith)
|
|
|
|
{
|
|
|
|
if (lith->page0) {
|
|
|
|
iounmap(lith->page0);
|
|
|
|
lith->page0 = NULL;
|
|
|
|
}
|
|
|
|
if (lith->page1) {
|
|
|
|
iounmap(lith->page1);
|
|
|
|
lith->page1 = NULL;
|
|
|
|
}
|
|
|
|
if (lith->page2) {
|
|
|
|
iounmap(lith->page2);
|
|
|
|
lith->page2 = NULL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2006-03-26 17:37:37 +08:00
|
|
|
/*
|
|
|
|
* li_create initializes the lithium_t structure and sets up vm mappings
|
|
|
|
* to access the registers.
|
|
|
|
* Returns 0 on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int __init li_create(lithium_t *lith, unsigned long baseaddr)
|
|
|
|
{
|
|
|
|
spin_lock_init(&lith->lock);
|
|
|
|
lith->page0 = ioremap_nocache(baseaddr + LI_PAGE0_OFFSET, PAGE_SIZE);
|
|
|
|
lith->page1 = ioremap_nocache(baseaddr + LI_PAGE1_OFFSET, PAGE_SIZE);
|
|
|
|
lith->page2 = ioremap_nocache(baseaddr + LI_PAGE2_OFFSET, PAGE_SIZE);
|
|
|
|
if (!lith->page0 || !lith->page1 || !lith->page2) {
|
|
|
|
li_destroy(lith);
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2005-04-17 06:20:36 +08:00
|
|
|
/*
|
|
|
|
* basic register accessors - read/write long/byte
|
|
|
|
*/
|
|
|
|
|
|
|
|
static __inline__ unsigned long li_readl(lithium_t *lith, int off)
|
|
|
|
{
|
|
|
|
return * (volatile unsigned long *) (lith->page0 + off);
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ unsigned char li_readb(lithium_t *lith, int off)
|
|
|
|
{
|
|
|
|
return * (volatile unsigned char *) (lith->page0 + off);
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ void li_writel(lithium_t *lith, int off, unsigned long val)
|
|
|
|
{
|
|
|
|
* (volatile unsigned long *) (lith->page0 + off) = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ void li_writeb(lithium_t *lith, int off, unsigned char val)
|
|
|
|
{
|
|
|
|
* (volatile unsigned char *) (lith->page0 + off) = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* High Level Lithium Access */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Lithium DMA Notes
|
|
|
|
*
|
|
|
|
* Lithium has two dedicated DMA channels for audio. They are known
|
|
|
|
* as comm1 and comm2 (communication areas 1 and 2). Comm1 is for
|
|
|
|
* input, and comm2 is for output. Each is controlled by three
|
|
|
|
* registers: BASE (base address), CFG (config) and CCTL
|
|
|
|
* (config/control).
|
|
|
|
*
|
|
|
|
* Each DMA channel points to a physically contiguous ring buffer in
|
|
|
|
* main memory of up to 8 Kbytes. (This driver always uses 8 Kb.)
|
|
|
|
* There are three pointers into the ring buffer: read, write, and
|
|
|
|
* trigger. The pointers are 8 bits each. Each pointer points to
|
|
|
|
* 32-byte "chunks" of data. The DMA engine moves 32 bytes at a time,
|
|
|
|
* so there is no finer-granularity control.
|
|
|
|
*
|
|
|
|
* In comm1, the hardware updates the write ptr, and software updates
|
|
|
|
* the read ptr. In comm2, it's the opposite: hardware updates the
|
|
|
|
* read ptr, and software updates the write ptr. I designate the
|
|
|
|
* hardware-updated ptr as the hwptr, and the software-updated ptr as
|
|
|
|
* the swptr.
|
|
|
|
*
|
|
|
|
* The trigger ptr and trigger mask are used to trigger interrupts.
|
|
|
|
* From the Lithium spec, section 5.6.8, revision of 12/15/1998:
|
|
|
|
*
|
|
|
|
* Trigger Mask Value
|
|
|
|
*
|
|
|
|
* A three bit wide field that represents a power of two mask
|
|
|
|
* that is used whenever the trigger pointer is compared to its
|
|
|
|
* respective read or write pointer. A value of zero here
|
|
|
|
* implies a mask of 0xFF and a value of seven implies a mask
|
|
|
|
* 0x01. This value can be used to sub-divide the ring buffer
|
|
|
|
* into pie sections so that interrupts monitor the progress of
|
|
|
|
* hardware from section to section.
|
|
|
|
*
|
|
|
|
* My interpretation of that is, whenever the hw ptr is updated, it is
|
|
|
|
* compared with the trigger ptr, and the result is masked by the
|
|
|
|
* trigger mask. (Actually, by the complement of the trigger mask.)
|
|
|
|
* If the result is zero, an interrupt is triggered. I.e., interrupt
|
|
|
|
* if ((hwptr & ~mask) == (trptr & ~mask)). The mask is formed from
|
|
|
|
* the trigger register value as mask = (1 << (8 - tmreg)) - 1.
|
|
|
|
*
|
|
|
|
* In yet different words, setting tmreg to 0 causes an interrupt after
|
|
|
|
* every 256 DMA chunks (8192 bytes) or once per traversal of the
|
|
|
|
* ring buffer. Setting it to 7 caues an interrupt every 2 DMA chunks
|
|
|
|
* (64 bytes) or 128 times per traversal of the ring buffer.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* Lithium register offsets and bit definitions */
|
|
|
|
|
|
|
|
#define LI_HOST_CONTROLLER 0x000
|
|
|
|
# define LI_HC_RESET 0x00008000
|
|
|
|
# define LI_HC_LINK_ENABLE 0x00004000
|
|
|
|
# define LI_HC_LINK_FAILURE 0x00000004
|
|
|
|
# define LI_HC_LINK_CODEC 0x00000002
|
|
|
|
# define LI_HC_LINK_READY 0x00000001
|
|
|
|
|
|
|
|
#define LI_INTR_STATUS 0x010
|
|
|
|
#define LI_INTR_MASK 0x014
|
|
|
|
# define LI_INTR_LINK_ERR 0x00008000
|
|
|
|
# define LI_INTR_COMM2_TRIG 0x00000008
|
|
|
|
# define LI_INTR_COMM2_UNDERFLOW 0x00000004
|
|
|
|
# define LI_INTR_COMM1_TRIG 0x00000002
|
|
|
|
# define LI_INTR_COMM1_OVERFLOW 0x00000001
|
|
|
|
|
|
|
|
#define LI_CODEC_COMMAND 0x018
|
|
|
|
# define LI_CC_BUSY 0x00008000
|
|
|
|
# define LI_CC_DIR 0x00000080
|
|
|
|
# define LI_CC_DIR_RD LI_CC_DIR
|
|
|
|
# define LI_CC_DIR_WR (!LI_CC_DIR)
|
|
|
|
# define LI_CC_ADDR_MASK 0x0000007F
|
|
|
|
|
|
|
|
#define LI_CODEC_DATA 0x01C
|
|
|
|
|
|
|
|
#define LI_COMM1_BASE 0x100
|
|
|
|
#define LI_COMM1_CTL 0x104
|
|
|
|
# define LI_CCTL_RESET 0x80000000
|
|
|
|
# define LI_CCTL_SIZE 0x70000000
|
|
|
|
# define LI_CCTL_DMA_ENABLE 0x08000000
|
|
|
|
# define LI_CCTL_TMASK 0x07000000 /* trigger mask */
|
|
|
|
# define LI_CCTL_TPTR 0x00FF0000 /* trigger pointer */
|
|
|
|
# define LI_CCTL_RPTR 0x0000FF00
|
|
|
|
# define LI_CCTL_WPTR 0x000000FF
|
|
|
|
#define LI_COMM1_CFG 0x108
|
|
|
|
# define LI_CCFG_LOCK 0x00008000
|
|
|
|
# define LI_CCFG_SLOT 0x00000070
|
|
|
|
# define LI_CCFG_DIRECTION 0x00000008
|
|
|
|
# define LI_CCFG_DIR_IN (!LI_CCFG_DIRECTION)
|
|
|
|
# define LI_CCFG_DIR_OUT LI_CCFG_DIRECTION
|
|
|
|
# define LI_CCFG_MODE 0x00000004
|
|
|
|
# define LI_CCFG_MODE_MONO (!LI_CCFG_MODE)
|
|
|
|
# define LI_CCFG_MODE_STEREO LI_CCFG_MODE
|
|
|
|
# define LI_CCFG_FORMAT 0x00000003
|
|
|
|
# define LI_CCFG_FMT_8BIT 0x00000000
|
|
|
|
# define LI_CCFG_FMT_16BIT 0x00000001
|
|
|
|
#define LI_COMM2_BASE 0x10C
|
|
|
|
#define LI_COMM2_CTL 0x110
|
|
|
|
/* bit definitions are the same as LI_COMM1_CTL */
|
|
|
|
#define LI_COMM2_CFG 0x114
|
|
|
|
/* bit definitions are the same as LI_COMM1_CFG */
|
|
|
|
|
|
|
|
#define LI_UST_LOW 0x200 /* 64-bit Unadjusted System Time is */
|
|
|
|
#define LI_UST_HIGH 0x204 /* microseconds since boot */
|
|
|
|
|
|
|
|
#define LI_AUDIO1_UST 0x300 /* UST-MSC pairs */
|
|
|
|
#define LI_AUDIO1_MSC 0x304 /* MSC (Media Stream Counter) */
|
|
|
|
#define LI_AUDIO2_UST 0x308 /* counts samples actually */
|
|
|
|
#define LI_AUDIO2_MSC 0x30C /* processed as of time UST */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Lithium's DMA engine operates on chunks of 32 bytes. We call that
|
|
|
|
* a DMACHUNK.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define DMACHUNK_SHIFT 5
|
|
|
|
#define DMACHUNK_SIZE (1 << DMACHUNK_SHIFT)
|
|
|
|
#define BYTES_TO_CHUNKS(bytes) ((bytes) >> DMACHUNK_SHIFT)
|
|
|
|
#define CHUNKS_TO_BYTES(chunks) ((chunks) << DMACHUNK_SHIFT)
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Two convenient macros to shift bitfields into/out of position.
|
|
|
|
*
|
|
|
|
* Observe that (mask & -mask) is (1 << low_set_bit_of(mask)).
|
|
|
|
* As long as mask is constant, we trust the compiler will change the
|
|
|
|
* multipy and divide into shifts.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define SHIFT_FIELD(val, mask) (((val) * ((mask) & -(mask))) & (mask))
|
|
|
|
#define UNSHIFT_FIELD(val, mask) (((val) & (mask)) / ((mask) & -(mask)))
|
|
|
|
|
|
|
|
/*
|
|
|
|
* dma_chan_desc is invariant information about a Lithium
|
|
|
|
* DMA channel. There are two instances, li_comm1 and li_comm2.
|
|
|
|
*
|
|
|
|
* Note that the CCTL register fields are write ptr and read ptr, but what
|
|
|
|
* we care about are which pointer is updated by software and which by
|
|
|
|
* hardware.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct dma_chan_desc {
|
|
|
|
int basereg;
|
|
|
|
int cfgreg;
|
|
|
|
int ctlreg;
|
|
|
|
int hwptrreg;
|
|
|
|
int swptrreg;
|
|
|
|
int ustreg;
|
|
|
|
int mscreg;
|
|
|
|
unsigned long swptrmask;
|
|
|
|
int ad1843_slot;
|
|
|
|
int direction; /* LI_CCTL_DIR_IN/OUT */
|
|
|
|
} dma_chan_desc_t;
|
|
|
|
|
|
|
|
static const dma_chan_desc_t li_comm1 = {
|
|
|
|
LI_COMM1_BASE, /* base register offset */
|
|
|
|
LI_COMM1_CFG, /* config register offset */
|
|
|
|
LI_COMM1_CTL, /* control register offset */
|
|
|
|
LI_COMM1_CTL + 0, /* hw ptr reg offset (write ptr) */
|
|
|
|
LI_COMM1_CTL + 1, /* sw ptr reg offset (read ptr) */
|
|
|
|
LI_AUDIO1_UST, /* ust reg offset */
|
|
|
|
LI_AUDIO1_MSC, /* msc reg offset */
|
|
|
|
LI_CCTL_RPTR, /* sw ptr bitmask in ctlval */
|
|
|
|
2, /* ad1843 serial slot */
|
|
|
|
LI_CCFG_DIR_IN /* direction */
|
|
|
|
};
|
|
|
|
|
|
|
|
static const dma_chan_desc_t li_comm2 = {
|
|
|
|
LI_COMM2_BASE, /* base register offset */
|
|
|
|
LI_COMM2_CFG, /* config register offset */
|
|
|
|
LI_COMM2_CTL, /* control register offset */
|
|
|
|
LI_COMM2_CTL + 1, /* hw ptr reg offset (read ptr) */
|
|
|
|
LI_COMM2_CTL + 0, /* sw ptr reg offset (writr ptr) */
|
|
|
|
LI_AUDIO2_UST, /* ust reg offset */
|
|
|
|
LI_AUDIO2_MSC, /* msc reg offset */
|
|
|
|
LI_CCTL_WPTR, /* sw ptr bitmask in ctlval */
|
|
|
|
2, /* ad1843 serial slot */
|
|
|
|
LI_CCFG_DIR_OUT /* direction */
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* dma_chan is variable information about a Lithium DMA channel.
|
|
|
|
*
|
|
|
|
* The desc field points to invariant information.
|
|
|
|
* The lith field points to a lithium_t which is passed
|
|
|
|
* to li_read* and li_write* to access the registers.
|
|
|
|
* The *val fields shadow the lithium registers' contents.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct dma_chan {
|
|
|
|
const dma_chan_desc_t *desc;
|
|
|
|
lithium_t *lith;
|
|
|
|
unsigned long baseval;
|
|
|
|
unsigned long cfgval;
|
|
|
|
unsigned long ctlval;
|
|
|
|
} dma_chan_t;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* ustmsc is a UST/MSC pair (Unadjusted System Time/Media Stream Counter).
|
|
|
|
* UST is time in microseconds since the system booted, and MSC is a
|
|
|
|
* counter that increments with every audio sample.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct ustmsc {
|
|
|
|
unsigned long long ust;
|
|
|
|
unsigned long msc;
|
|
|
|
} ustmsc_t;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* li_ad1843_wait waits until lithium says the AD1843 register
|
|
|
|
* exchange is not busy. Returns 0 on success, -EBUSY on timeout.
|
|
|
|
*
|
|
|
|
* Locking: must be called with lithium_lock held.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int li_ad1843_wait(lithium_t *lith)
|
|
|
|
{
|
|
|
|
unsigned long later = jiffies + 2;
|
|
|
|
while (li_readl(lith, LI_CODEC_COMMAND) & LI_CC_BUSY)
|
|
|
|
if (time_after_eq(jiffies, later))
|
|
|
|
return -EBUSY;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* li_read_ad1843_reg returns the current contents of a 16 bit AD1843 register.
|
|
|
|
*
|
|
|
|
* Returns unsigned register value on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int li_read_ad1843_reg(lithium_t *lith, int reg)
|
|
|
|
{
|
|
|
|
int val;
|
|
|
|
|
|
|
|
ASSERT(!in_interrupt());
|
|
|
|
spin_lock(&lith->lock);
|
|
|
|
{
|
|
|
|
val = li_ad1843_wait(lith);
|
|
|
|
if (val == 0) {
|
|
|
|
li_writel(lith, LI_CODEC_COMMAND, LI_CC_DIR_RD | reg);
|
|
|
|
val = li_ad1843_wait(lith);
|
|
|
|
}
|
|
|
|
if (val == 0)
|
|
|
|
val = li_readl(lith, LI_CODEC_DATA);
|
|
|
|
}
|
|
|
|
spin_unlock(&lith->lock);
|
|
|
|
|
|
|
|
DBGXV("li_read_ad1843_reg(lith=0x%p, reg=%d) returns 0x%04x\n",
|
|
|
|
lith, reg, val);
|
|
|
|
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* li_write_ad1843_reg writes the specified value to a 16 bit AD1843 register.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void li_write_ad1843_reg(lithium_t *lith, int reg, int newval)
|
|
|
|
{
|
|
|
|
spin_lock(&lith->lock);
|
|
|
|
{
|
|
|
|
if (li_ad1843_wait(lith) == 0) {
|
|
|
|
li_writel(lith, LI_CODEC_DATA, newval);
|
|
|
|
li_writel(lith, LI_CODEC_COMMAND, LI_CC_DIR_WR | reg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
spin_unlock(&lith->lock);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* li_setup_dma calculates all the register settings for DMA in a particular
|
|
|
|
* mode. It takes too many arguments.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void li_setup_dma(dma_chan_t *chan,
|
|
|
|
const dma_chan_desc_t *desc,
|
|
|
|
lithium_t *lith,
|
|
|
|
unsigned long buffer_paddr,
|
|
|
|
int bufshift,
|
|
|
|
int fragshift,
|
|
|
|
int channels,
|
|
|
|
int sampsize)
|
|
|
|
{
|
|
|
|
unsigned long mode, format;
|
|
|
|
unsigned long size, tmask;
|
|
|
|
|
|
|
|
DBGEV("(chan=0x%p, desc=0x%p, lith=0x%p, buffer_paddr=0x%lx, "
|
|
|
|
"bufshift=%d, fragshift=%d, channels=%d, sampsize=%d)\n",
|
|
|
|
chan, desc, lith, buffer_paddr,
|
|
|
|
bufshift, fragshift, channels, sampsize);
|
|
|
|
|
|
|
|
/* Reset the channel first. */
|
|
|
|
|
|
|
|
li_writel(lith, desc->ctlreg, LI_CCTL_RESET);
|
|
|
|
|
|
|
|
ASSERT(channels == 1 || channels == 2);
|
|
|
|
if (channels == 2)
|
|
|
|
mode = LI_CCFG_MODE_STEREO;
|
|
|
|
else
|
|
|
|
mode = LI_CCFG_MODE_MONO;
|
|
|
|
ASSERT(sampsize == 1 || sampsize == 2);
|
|
|
|
if (sampsize == 2)
|
|
|
|
format = LI_CCFG_FMT_16BIT;
|
|
|
|
else
|
|
|
|
format = LI_CCFG_FMT_8BIT;
|
|
|
|
chan->desc = desc;
|
|
|
|
chan->lith = lith;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Lithium DMA address register takes a 40-bit physical
|
|
|
|
* address, right-shifted by 8 so it fits in 32 bits. Bit 37
|
|
|
|
* must be set -- it enables cache coherence.
|
|
|
|
*/
|
|
|
|
|
|
|
|
ASSERT(!(buffer_paddr & 0xFF));
|
|
|
|
chan->baseval = (buffer_paddr >> 8) | 1 << (37 - 8);
|
|
|
|
|
2009-08-26 18:01:20 +08:00
|
|
|
chan->cfgval = ((chan->cfgval & ~LI_CCFG_LOCK) |
|
2005-04-17 06:20:36 +08:00
|
|
|
SHIFT_FIELD(desc->ad1843_slot, LI_CCFG_SLOT) |
|
|
|
|
desc->direction |
|
|
|
|
mode |
|
|
|
|
format);
|
|
|
|
|
|
|
|
size = bufshift - 6;
|
|
|
|
tmask = 13 - fragshift; /* See Lithium DMA Notes above. */
|
|
|
|
ASSERT(size >= 2 && size <= 7);
|
|
|
|
ASSERT(tmask >= 1 && tmask <= 7);
|
2009-08-26 18:01:20 +08:00
|
|
|
chan->ctlval = ((chan->ctlval & ~LI_CCTL_RESET) |
|
2005-04-17 06:20:36 +08:00
|
|
|
SHIFT_FIELD(size, LI_CCTL_SIZE) |
|
2009-08-26 18:01:20 +08:00
|
|
|
(chan->ctlval & ~LI_CCTL_DMA_ENABLE) |
|
2005-04-17 06:20:36 +08:00
|
|
|
SHIFT_FIELD(tmask, LI_CCTL_TMASK) |
|
|
|
|
SHIFT_FIELD(0, LI_CCTL_TPTR));
|
|
|
|
|
|
|
|
DBGPV("basereg 0x%x = 0x%lx\n", desc->basereg, chan->baseval);
|
|
|
|
DBGPV("cfgreg 0x%x = 0x%lx\n", desc->cfgreg, chan->cfgval);
|
|
|
|
DBGPV("ctlreg 0x%x = 0x%lx\n", desc->ctlreg, chan->ctlval);
|
|
|
|
|
|
|
|
li_writel(lith, desc->basereg, chan->baseval);
|
|
|
|
li_writel(lith, desc->cfgreg, chan->cfgval);
|
|
|
|
li_writel(lith, desc->ctlreg, chan->ctlval);
|
|
|
|
|
|
|
|
DBGRV();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void li_shutdown_dma(dma_chan_t *chan)
|
|
|
|
{
|
|
|
|
lithium_t *lith = chan->lith;
|
|
|
|
void * lith1 = lith->page1;
|
|
|
|
|
|
|
|
DBGEV("(chan=0x%p)\n", chan);
|
|
|
|
|
|
|
|
chan->ctlval &= ~LI_CCTL_DMA_ENABLE;
|
|
|
|
DBGPV("ctlreg 0x%x = 0x%lx\n", chan->desc->ctlreg, chan->ctlval);
|
|
|
|
li_writel(lith, chan->desc->ctlreg, chan->ctlval);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Offset 0x500 on Lithium page 1 is an undocumented,
|
|
|
|
* unsupported register that holds the zero sample value.
|
|
|
|
* Lithium is supposed to output zero samples when DMA is
|
|
|
|
* inactive, and repeat the last sample when DMA underflows.
|
|
|
|
* But it has a bug, where, after underflow occurs, the zero
|
|
|
|
* sample is not reset.
|
|
|
|
*
|
|
|
|
* I expect this to break in a future rev of Lithium.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (lith1 && chan->desc->direction == LI_CCFG_DIR_OUT)
|
|
|
|
* (volatile unsigned long *) (lith1 + 0x500) = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* li_activate_dma always starts dma at the beginning of the buffer.
|
|
|
|
*
|
|
|
|
* N.B., these may be called from interrupt.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static __inline__ void li_activate_dma(dma_chan_t *chan)
|
|
|
|
{
|
|
|
|
chan->ctlval |= LI_CCTL_DMA_ENABLE;
|
|
|
|
DBGPV("ctlval = 0x%lx\n", chan->ctlval);
|
|
|
|
li_writel(chan->lith, chan->desc->ctlreg, chan->ctlval);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void li_deactivate_dma(dma_chan_t *chan)
|
|
|
|
{
|
|
|
|
lithium_t *lith = chan->lith;
|
|
|
|
void * lith2 = lith->page2;
|
|
|
|
|
|
|
|
chan->ctlval &= ~(LI_CCTL_DMA_ENABLE | LI_CCTL_RPTR | LI_CCTL_WPTR);
|
|
|
|
DBGPV("ctlval = 0x%lx\n", chan->ctlval);
|
|
|
|
DBGPV("ctlreg 0x%x = 0x%lx\n", chan->desc->ctlreg, chan->ctlval);
|
|
|
|
li_writel(lith, chan->desc->ctlreg, chan->ctlval);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Offsets 0x98 and 0x9C on Lithium page 2 are undocumented,
|
|
|
|
* unsupported registers that are internal copies of the DMA
|
|
|
|
* read and write pointers. Because of a Lithium bug, these
|
|
|
|
* registers aren't zeroed correctly when DMA is shut off. So
|
|
|
|
* we whack them directly.
|
|
|
|
*
|
|
|
|
* I expect this to break in a future rev of Lithium.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (lith2 && chan->desc->direction == LI_CCFG_DIR_OUT) {
|
|
|
|
* (volatile unsigned long *) (lith2 + 0x98) = 0;
|
|
|
|
* (volatile unsigned long *) (lith2 + 0x9C) = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* read/write the ring buffer pointers. These routines' arguments and results
|
|
|
|
* are byte offsets from the beginning of the ring buffer.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static __inline__ int li_read_swptr(dma_chan_t *chan)
|
|
|
|
{
|
|
|
|
const unsigned long mask = chan->desc->swptrmask;
|
|
|
|
|
|
|
|
return CHUNKS_TO_BYTES(UNSHIFT_FIELD(chan->ctlval, mask));
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ int li_read_hwptr(dma_chan_t *chan)
|
|
|
|
{
|
|
|
|
return CHUNKS_TO_BYTES(li_readb(chan->lith, chan->desc->hwptrreg));
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ void li_write_swptr(dma_chan_t *chan, int val)
|
|
|
|
{
|
|
|
|
const unsigned long mask = chan->desc->swptrmask;
|
|
|
|
|
|
|
|
ASSERT(!(val & ~CHUNKS_TO_BYTES(0xFF)));
|
|
|
|
val = BYTES_TO_CHUNKS(val);
|
|
|
|
chan->ctlval = (chan->ctlval & ~mask) | SHIFT_FIELD(val, mask);
|
|
|
|
li_writeb(chan->lith, chan->desc->swptrreg, val);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* li_read_USTMSC() returns a UST/MSC pair for the given channel. */
|
|
|
|
|
|
|
|
static void li_read_USTMSC(dma_chan_t *chan, ustmsc_t *ustmsc)
|
|
|
|
{
|
|
|
|
lithium_t *lith = chan->lith;
|
|
|
|
const dma_chan_desc_t *desc = chan->desc;
|
|
|
|
unsigned long now_low, now_high0, now_high1, chan_ust;
|
|
|
|
|
|
|
|
spin_lock(&lith->lock);
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* retry until we do all five reads without the
|
|
|
|
* high word changing. (High word increments
|
|
|
|
* every 2^32 microseconds, i.e., not often)
|
|
|
|
*/
|
|
|
|
do {
|
|
|
|
now_high0 = li_readl(lith, LI_UST_HIGH);
|
|
|
|
now_low = li_readl(lith, LI_UST_LOW);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Lithium guarantees these two reads will be
|
|
|
|
* atomic -- ust will not increment after msc
|
|
|
|
* is read.
|
|
|
|
*/
|
|
|
|
|
|
|
|
ustmsc->msc = li_readl(lith, desc->mscreg);
|
|
|
|
chan_ust = li_readl(lith, desc->ustreg);
|
|
|
|
|
|
|
|
now_high1 = li_readl(lith, LI_UST_HIGH);
|
|
|
|
} while (now_high0 != now_high1);
|
|
|
|
}
|
|
|
|
spin_unlock(&lith->lock);
|
|
|
|
ustmsc->ust = ((unsigned long long) now_high0 << 32 | chan_ust);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void li_enable_interrupts(lithium_t *lith, unsigned int mask)
|
|
|
|
{
|
|
|
|
DBGEV("(lith=0x%p, mask=0x%x)\n", lith, mask);
|
|
|
|
|
|
|
|
/* clear any already-pending interrupts. */
|
|
|
|
|
|
|
|
li_writel(lith, LI_INTR_STATUS, mask);
|
|
|
|
|
|
|
|
/* enable the interrupts. */
|
|
|
|
|
|
|
|
mask |= li_readl(lith, LI_INTR_MASK);
|
|
|
|
li_writel(lith, LI_INTR_MASK, mask);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void li_disable_interrupts(lithium_t *lith, unsigned int mask)
|
|
|
|
{
|
|
|
|
unsigned int keepmask;
|
|
|
|
|
|
|
|
DBGEV("(lith=0x%p, mask=0x%x)\n", lith, mask);
|
|
|
|
|
|
|
|
/* disable the interrupts */
|
|
|
|
|
|
|
|
keepmask = li_readl(lith, LI_INTR_MASK) & ~mask;
|
|
|
|
li_writel(lith, LI_INTR_MASK, keepmask);
|
|
|
|
|
|
|
|
/* clear any pending interrupts. */
|
|
|
|
|
|
|
|
li_writel(lith, LI_INTR_STATUS, mask);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Get the interrupt status and clear all pending interrupts. */
|
|
|
|
|
|
|
|
static unsigned int li_get_clear_intr_status(lithium_t *lith)
|
|
|
|
{
|
|
|
|
unsigned int status;
|
|
|
|
|
|
|
|
status = li_readl(lith, LI_INTR_STATUS);
|
|
|
|
li_writel(lith, LI_INTR_STATUS, ~0);
|
|
|
|
return status & li_readl(lith, LI_INTR_MASK);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int li_init(lithium_t *lith)
|
|
|
|
{
|
|
|
|
/* 1. System power supplies stabilize. */
|
|
|
|
|
|
|
|
/* 2. Assert the ~RESET signal. */
|
|
|
|
|
|
|
|
li_writel(lith, LI_HOST_CONTROLLER, LI_HC_RESET);
|
|
|
|
udelay(1);
|
|
|
|
|
|
|
|
/* 3. Deassert the ~RESET signal and enter a wait period to allow
|
|
|
|
the AD1843 internal clocks and the external crystal oscillator
|
|
|
|
to stabilize. */
|
|
|
|
|
|
|
|
li_writel(lith, LI_HOST_CONTROLLER, LI_HC_LINK_ENABLE);
|
|
|
|
udelay(1);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* AD1843 access */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* AD1843 bitfield definitions. All are named as in the AD1843 data
|
|
|
|
* sheet, with ad1843_ prepended and individual bit numbers removed.
|
|
|
|
*
|
|
|
|
* E.g., bits LSS0 through LSS2 become ad1843_LSS.
|
|
|
|
*
|
|
|
|
* Only the bitfields we need are defined.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct ad1843_bitfield {
|
|
|
|
char reg;
|
|
|
|
char lo_bit;
|
|
|
|
char nbits;
|
|
|
|
} ad1843_bitfield_t;
|
|
|
|
|
|
|
|
static const ad1843_bitfield_t
|
|
|
|
ad1843_PDNO = { 0, 14, 1 }, /* Converter Power-Down Flag */
|
|
|
|
ad1843_INIT = { 0, 15, 1 }, /* Clock Initialization Flag */
|
|
|
|
ad1843_RIG = { 2, 0, 4 }, /* Right ADC Input Gain */
|
|
|
|
ad1843_RMGE = { 2, 4, 1 }, /* Right ADC Mic Gain Enable */
|
|
|
|
ad1843_RSS = { 2, 5, 3 }, /* Right ADC Source Select */
|
|
|
|
ad1843_LIG = { 2, 8, 4 }, /* Left ADC Input Gain */
|
|
|
|
ad1843_LMGE = { 2, 12, 1 }, /* Left ADC Mic Gain Enable */
|
|
|
|
ad1843_LSS = { 2, 13, 3 }, /* Left ADC Source Select */
|
|
|
|
ad1843_RX1M = { 4, 0, 5 }, /* Right Aux 1 Mix Gain/Atten */
|
|
|
|
ad1843_RX1MM = { 4, 7, 1 }, /* Right Aux 1 Mix Mute */
|
|
|
|
ad1843_LX1M = { 4, 8, 5 }, /* Left Aux 1 Mix Gain/Atten */
|
|
|
|
ad1843_LX1MM = { 4, 15, 1 }, /* Left Aux 1 Mix Mute */
|
|
|
|
ad1843_RX2M = { 5, 0, 5 }, /* Right Aux 2 Mix Gain/Atten */
|
|
|
|
ad1843_RX2MM = { 5, 7, 1 }, /* Right Aux 2 Mix Mute */
|
|
|
|
ad1843_LX2M = { 5, 8, 5 }, /* Left Aux 2 Mix Gain/Atten */
|
|
|
|
ad1843_LX2MM = { 5, 15, 1 }, /* Left Aux 2 Mix Mute */
|
|
|
|
ad1843_RMCM = { 7, 0, 5 }, /* Right Mic Mix Gain/Atten */
|
|
|
|
ad1843_RMCMM = { 7, 7, 1 }, /* Right Mic Mix Mute */
|
|
|
|
ad1843_LMCM = { 7, 8, 5 }, /* Left Mic Mix Gain/Atten */
|
|
|
|
ad1843_LMCMM = { 7, 15, 1 }, /* Left Mic Mix Mute */
|
|
|
|
ad1843_HPOS = { 8, 4, 1 }, /* Headphone Output Voltage Swing */
|
|
|
|
ad1843_HPOM = { 8, 5, 1 }, /* Headphone Output Mute */
|
|
|
|
ad1843_RDA1G = { 9, 0, 6 }, /* Right DAC1 Analog/Digital Gain */
|
|
|
|
ad1843_RDA1GM = { 9, 7, 1 }, /* Right DAC1 Analog Mute */
|
|
|
|
ad1843_LDA1G = { 9, 8, 6 }, /* Left DAC1 Analog/Digital Gain */
|
|
|
|
ad1843_LDA1GM = { 9, 15, 1 }, /* Left DAC1 Analog Mute */
|
|
|
|
ad1843_RDA1AM = { 11, 7, 1 }, /* Right DAC1 Digital Mute */
|
|
|
|
ad1843_LDA1AM = { 11, 15, 1 }, /* Left DAC1 Digital Mute */
|
|
|
|
ad1843_ADLC = { 15, 0, 2 }, /* ADC Left Sample Rate Source */
|
|
|
|
ad1843_ADRC = { 15, 2, 2 }, /* ADC Right Sample Rate Source */
|
|
|
|
ad1843_DA1C = { 15, 8, 2 }, /* DAC1 Sample Rate Source */
|
|
|
|
ad1843_C1C = { 17, 0, 16 }, /* Clock 1 Sample Rate Select */
|
|
|
|
ad1843_C2C = { 20, 0, 16 }, /* Clock 1 Sample Rate Select */
|
|
|
|
ad1843_DAADL = { 25, 4, 2 }, /* Digital ADC Left Source Select */
|
|
|
|
ad1843_DAADR = { 25, 6, 2 }, /* Digital ADC Right Source Select */
|
|
|
|
ad1843_DRSFLT = { 25, 15, 1 }, /* Digital Reampler Filter Mode */
|
|
|
|
ad1843_ADLF = { 26, 0, 2 }, /* ADC Left Channel Data Format */
|
|
|
|
ad1843_ADRF = { 26, 2, 2 }, /* ADC Right Channel Data Format */
|
|
|
|
ad1843_ADTLK = { 26, 4, 1 }, /* ADC Transmit Lock Mode Select */
|
|
|
|
ad1843_SCF = { 26, 7, 1 }, /* SCLK Frequency Select */
|
|
|
|
ad1843_DA1F = { 26, 8, 2 }, /* DAC1 Data Format Select */
|
|
|
|
ad1843_DA1SM = { 26, 14, 1 }, /* DAC1 Stereo/Mono Mode Select */
|
|
|
|
ad1843_ADLEN = { 27, 0, 1 }, /* ADC Left Channel Enable */
|
|
|
|
ad1843_ADREN = { 27, 1, 1 }, /* ADC Right Channel Enable */
|
|
|
|
ad1843_AAMEN = { 27, 4, 1 }, /* Analog to Analog Mix Enable */
|
|
|
|
ad1843_ANAEN = { 27, 7, 1 }, /* Analog Channel Enable */
|
|
|
|
ad1843_DA1EN = { 27, 8, 1 }, /* DAC1 Enable */
|
|
|
|
ad1843_DA2EN = { 27, 9, 1 }, /* DAC2 Enable */
|
|
|
|
ad1843_C1EN = { 28, 11, 1 }, /* Clock Generator 1 Enable */
|
|
|
|
ad1843_C2EN = { 28, 12, 1 }, /* Clock Generator 2 Enable */
|
|
|
|
ad1843_PDNI = { 28, 15, 1 }; /* Converter Power Down */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The various registers of the AD1843 use three different formats for
|
|
|
|
* specifying gain. The ad1843_gain structure parameterizes the
|
|
|
|
* formats.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct ad1843_gain {
|
|
|
|
|
|
|
|
int negative; /* nonzero if gain is negative. */
|
|
|
|
const ad1843_bitfield_t *lfield;
|
|
|
|
const ad1843_bitfield_t *rfield;
|
|
|
|
|
|
|
|
} ad1843_gain_t;
|
|
|
|
|
|
|
|
static const ad1843_gain_t ad1843_gain_RECLEV
|
|
|
|
= { 0, &ad1843_LIG, &ad1843_RIG };
|
|
|
|
static const ad1843_gain_t ad1843_gain_LINE
|
|
|
|
= { 1, &ad1843_LX1M, &ad1843_RX1M };
|
|
|
|
static const ad1843_gain_t ad1843_gain_CD
|
|
|
|
= { 1, &ad1843_LX2M, &ad1843_RX2M };
|
|
|
|
static const ad1843_gain_t ad1843_gain_MIC
|
|
|
|
= { 1, &ad1843_LMCM, &ad1843_RMCM };
|
|
|
|
static const ad1843_gain_t ad1843_gain_PCM
|
|
|
|
= { 1, &ad1843_LDA1G, &ad1843_RDA1G };
|
|
|
|
|
|
|
|
/* read the current value of an AD1843 bitfield. */
|
|
|
|
|
|
|
|
static int ad1843_read_bits(lithium_t *lith, const ad1843_bitfield_t *field)
|
|
|
|
{
|
|
|
|
int w = li_read_ad1843_reg(lith, field->reg);
|
|
|
|
int val = w >> field->lo_bit & ((1 << field->nbits) - 1);
|
|
|
|
|
|
|
|
DBGXV("ad1843_read_bits(lith=0x%p, field->{%d %d %d}) returns 0x%x\n",
|
|
|
|
lith, field->reg, field->lo_bit, field->nbits, val);
|
|
|
|
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* write a new value to an AD1843 bitfield and return the old value.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int ad1843_write_bits(lithium_t *lith,
|
|
|
|
const ad1843_bitfield_t *field,
|
|
|
|
int newval)
|
|
|
|
{
|
|
|
|
int w = li_read_ad1843_reg(lith, field->reg);
|
|
|
|
int mask = ((1 << field->nbits) - 1) << field->lo_bit;
|
|
|
|
int oldval = (w & mask) >> field->lo_bit;
|
|
|
|
int newbits = (newval << field->lo_bit) & mask;
|
|
|
|
w = (w & ~mask) | newbits;
|
|
|
|
(void) li_write_ad1843_reg(lith, field->reg, w);
|
|
|
|
|
|
|
|
DBGXV("ad1843_write_bits(lith=0x%p, field->{%d %d %d}, val=0x%x) "
|
|
|
|
"returns 0x%x\n",
|
|
|
|
lith, field->reg, field->lo_bit, field->nbits, newval,
|
|
|
|
oldval);
|
|
|
|
|
|
|
|
return oldval;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* ad1843_read_multi reads multiple bitfields from the same AD1843
|
|
|
|
* register. It uses a single read cycle to do it. (Reading the
|
|
|
|
* ad1843 requires 256 bit times at 12.288 MHz, or nearly 20
|
|
|
|
* microseconds.)
|
|
|
|
*
|
|
|
|
* Called ike this.
|
|
|
|
*
|
|
|
|
* ad1843_read_multi(lith, nfields,
|
|
|
|
* &ad1843_FIELD1, &val1,
|
|
|
|
* &ad1843_FIELD2, &val2, ...);
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void ad1843_read_multi(lithium_t *lith, int argcount, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
const ad1843_bitfield_t *fp;
|
|
|
|
int w = 0, mask, *value, reg = -1;
|
|
|
|
|
|
|
|
va_start(ap, argcount);
|
|
|
|
while (--argcount >= 0) {
|
|
|
|
fp = va_arg(ap, const ad1843_bitfield_t *);
|
|
|
|
value = va_arg(ap, int *);
|
|
|
|
if (reg == -1) {
|
|
|
|
reg = fp->reg;
|
|
|
|
w = li_read_ad1843_reg(lith, reg);
|
|
|
|
}
|
|
|
|
ASSERT(reg == fp->reg);
|
|
|
|
mask = (1 << fp->nbits) - 1;
|
|
|
|
*value = w >> fp->lo_bit & mask;
|
|
|
|
}
|
|
|
|
va_end(ap);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* ad1843_write_multi stores multiple bitfields into the same AD1843
|
|
|
|
* register. It uses one read and one write cycle to do it.
|
|
|
|
*
|
|
|
|
* Called like this.
|
|
|
|
*
|
|
|
|
* ad1843_write_multi(lith, nfields,
|
|
|
|
* &ad1843_FIELD1, val1,
|
|
|
|
* &ad1843_FIELF2, val2, ...);
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void ad1843_write_multi(lithium_t *lith, int argcount, ...)
|
|
|
|
{
|
|
|
|
va_list ap;
|
|
|
|
int reg;
|
|
|
|
const ad1843_bitfield_t *fp;
|
|
|
|
int value;
|
|
|
|
int w, m, mask, bits;
|
|
|
|
|
|
|
|
mask = 0;
|
|
|
|
bits = 0;
|
|
|
|
reg = -1;
|
|
|
|
|
|
|
|
va_start(ap, argcount);
|
|
|
|
while (--argcount >= 0) {
|
|
|
|
fp = va_arg(ap, const ad1843_bitfield_t *);
|
|
|
|
value = va_arg(ap, int);
|
|
|
|
if (reg == -1)
|
|
|
|
reg = fp->reg;
|
|
|
|
ASSERT(fp->reg == reg);
|
|
|
|
m = ((1 << fp->nbits) - 1) << fp->lo_bit;
|
|
|
|
mask |= m;
|
|
|
|
bits |= (value << fp->lo_bit) & m;
|
|
|
|
}
|
|
|
|
va_end(ap);
|
|
|
|
ASSERT(!(bits & ~mask));
|
|
|
|
if (~mask & 0xFFFF)
|
|
|
|
w = li_read_ad1843_reg(lith, reg);
|
|
|
|
else
|
|
|
|
w = 0;
|
|
|
|
w = (w & ~mask) | bits;
|
|
|
|
(void) li_write_ad1843_reg(lith, reg, w);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* ad1843_get_gain reads the specified register and extracts the gain value
|
|
|
|
* using the supplied gain type. It returns the gain in OSS format.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int ad1843_get_gain(lithium_t *lith, const ad1843_gain_t *gp)
|
|
|
|
{
|
|
|
|
int lg, rg;
|
|
|
|
unsigned short mask = (1 << gp->lfield->nbits) - 1;
|
|
|
|
|
|
|
|
ad1843_read_multi(lith, 2, gp->lfield, &lg, gp->rfield, &rg);
|
|
|
|
if (gp->negative) {
|
|
|
|
lg = mask - lg;
|
|
|
|
rg = mask - rg;
|
|
|
|
}
|
|
|
|
lg = (lg * 100 + (mask >> 1)) / mask;
|
|
|
|
rg = (rg * 100 + (mask >> 1)) / mask;
|
|
|
|
return lg << 0 | rg << 8;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Set an audio channel's gain. Converts from OSS format to AD1843's
|
|
|
|
* format.
|
|
|
|
*
|
|
|
|
* Returns the new gain, which may be lower than the old gain.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int ad1843_set_gain(lithium_t *lith,
|
|
|
|
const ad1843_gain_t *gp,
|
|
|
|
int newval)
|
|
|
|
{
|
|
|
|
unsigned short mask = (1 << gp->lfield->nbits) - 1;
|
|
|
|
|
|
|
|
int lg = newval >> 0 & 0xFF;
|
|
|
|
int rg = newval >> 8;
|
|
|
|
if (lg < 0 || lg > 100 || rg < 0 || rg > 100)
|
|
|
|
return -EINVAL;
|
|
|
|
lg = (lg * mask + (mask >> 1)) / 100;
|
|
|
|
rg = (rg * mask + (mask >> 1)) / 100;
|
|
|
|
if (gp->negative) {
|
|
|
|
lg = mask - lg;
|
|
|
|
rg = mask - rg;
|
|
|
|
}
|
|
|
|
ad1843_write_multi(lith, 2, gp->lfield, lg, gp->rfield, rg);
|
|
|
|
return ad1843_get_gain(lith, gp);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Returns the current recording source, in OSS format. */
|
|
|
|
|
|
|
|
static int ad1843_get_recsrc(lithium_t *lith)
|
|
|
|
{
|
|
|
|
int ls = ad1843_read_bits(lith, &ad1843_LSS);
|
|
|
|
|
|
|
|
switch (ls) {
|
|
|
|
case 1:
|
|
|
|
return SOUND_MASK_MIC;
|
|
|
|
case 2:
|
|
|
|
return SOUND_MASK_LINE;
|
|
|
|
case 3:
|
|
|
|
return SOUND_MASK_CD;
|
|
|
|
case 6:
|
|
|
|
return SOUND_MASK_PCM;
|
|
|
|
default:
|
|
|
|
ASSERT(0);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Enable/disable digital resample mode in the AD1843.
|
|
|
|
*
|
|
|
|
* The AD1843 requires that ADL, ADR, DA1 and DA2 be powered down
|
|
|
|
* while switching modes. So we save DA1's state (DA2's state is not
|
|
|
|
* interesting), power them down, switch into/out of resample mode,
|
|
|
|
* power them up, and restore state.
|
|
|
|
*
|
|
|
|
* This will cause audible glitches if D/A or A/D is going on, so the
|
|
|
|
* driver disallows that (in mixer_write_ioctl()).
|
|
|
|
*
|
|
|
|
* The open question is, is this worth doing? I'm leaving it in,
|
|
|
|
* because it's written, but...
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void ad1843_set_resample_mode(lithium_t *lith, int onoff)
|
|
|
|
{
|
|
|
|
/* Save DA1 mute and gain (addr 9 is DA1 analog gain/attenuation) */
|
|
|
|
int save_da1 = li_read_ad1843_reg(lith, 9);
|
|
|
|
|
|
|
|
/* Power down A/D and D/A. */
|
|
|
|
ad1843_write_multi(lith, 4,
|
|
|
|
&ad1843_DA1EN, 0,
|
|
|
|
&ad1843_DA2EN, 0,
|
|
|
|
&ad1843_ADLEN, 0,
|
|
|
|
&ad1843_ADREN, 0);
|
|
|
|
|
|
|
|
/* Switch mode */
|
|
|
|
ASSERT(onoff == 0 || onoff == 1);
|
|
|
|
ad1843_write_bits(lith, &ad1843_DRSFLT, onoff);
|
|
|
|
|
|
|
|
/* Power up A/D and D/A. */
|
|
|
|
ad1843_write_multi(lith, 3,
|
|
|
|
&ad1843_DA1EN, 1,
|
|
|
|
&ad1843_ADLEN, 1,
|
|
|
|
&ad1843_ADREN, 1);
|
|
|
|
|
|
|
|
/* Restore DA1 mute and gain. */
|
|
|
|
li_write_ad1843_reg(lith, 9, save_da1);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Set recording source. Arg newsrc specifies an OSS channel mask.
|
|
|
|
*
|
|
|
|
* The complication is that when we switch into/out of loopback mode
|
|
|
|
* (i.e., src = SOUND_MASK_PCM), we change the AD1843 into/out of
|
|
|
|
* digital resampling mode.
|
|
|
|
*
|
|
|
|
* Returns newsrc on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int ad1843_set_recsrc(lithium_t *lith, int newsrc)
|
|
|
|
{
|
|
|
|
int bits;
|
|
|
|
int oldbits;
|
|
|
|
|
|
|
|
switch (newsrc) {
|
|
|
|
case SOUND_MASK_PCM:
|
|
|
|
bits = 6;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MASK_MIC:
|
|
|
|
bits = 1;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MASK_LINE:
|
|
|
|
bits = 2;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MASK_CD:
|
|
|
|
bits = 3;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
oldbits = ad1843_read_bits(lith, &ad1843_LSS);
|
|
|
|
if (newsrc == SOUND_MASK_PCM && oldbits != 6) {
|
|
|
|
DBGP("enabling digital resample mode\n");
|
|
|
|
ad1843_set_resample_mode(lith, 1);
|
|
|
|
ad1843_write_multi(lith, 2,
|
|
|
|
&ad1843_DAADL, 2,
|
|
|
|
&ad1843_DAADR, 2);
|
|
|
|
} else if (newsrc != SOUND_MASK_PCM && oldbits == 6) {
|
|
|
|
DBGP("disabling digital resample mode\n");
|
|
|
|
ad1843_set_resample_mode(lith, 0);
|
|
|
|
ad1843_write_multi(lith, 2,
|
|
|
|
&ad1843_DAADL, 0,
|
|
|
|
&ad1843_DAADR, 0);
|
|
|
|
}
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LSS, bits, &ad1843_RSS, bits);
|
|
|
|
return newsrc;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Return current output sources, in OSS format.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int ad1843_get_outsrc(lithium_t *lith)
|
|
|
|
{
|
|
|
|
int pcm, line, mic, cd;
|
|
|
|
|
|
|
|
pcm = ad1843_read_bits(lith, &ad1843_LDA1GM) ? 0 : SOUND_MASK_PCM;
|
|
|
|
line = ad1843_read_bits(lith, &ad1843_LX1MM) ? 0 : SOUND_MASK_LINE;
|
|
|
|
cd = ad1843_read_bits(lith, &ad1843_LX2MM) ? 0 : SOUND_MASK_CD;
|
|
|
|
mic = ad1843_read_bits(lith, &ad1843_LMCMM) ? 0 : SOUND_MASK_MIC;
|
|
|
|
|
|
|
|
return pcm | line | cd | mic;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Set output sources. Arg is a mask of active sources in OSS format.
|
|
|
|
*
|
|
|
|
* Returns source mask on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int ad1843_set_outsrc(lithium_t *lith, int mask)
|
|
|
|
{
|
|
|
|
int pcm, line, mic, cd;
|
|
|
|
|
|
|
|
if (mask & ~(SOUND_MASK_PCM | SOUND_MASK_LINE |
|
|
|
|
SOUND_MASK_CD | SOUND_MASK_MIC))
|
|
|
|
return -EINVAL;
|
|
|
|
pcm = (mask & SOUND_MASK_PCM) ? 0 : 1;
|
|
|
|
line = (mask & SOUND_MASK_LINE) ? 0 : 1;
|
|
|
|
mic = (mask & SOUND_MASK_MIC) ? 0 : 1;
|
|
|
|
cd = (mask & SOUND_MASK_CD) ? 0 : 1;
|
|
|
|
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LDA1GM, pcm, &ad1843_RDA1GM, pcm);
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LX1MM, line, &ad1843_RX1MM, line);
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LX2MM, cd, &ad1843_RX2MM, cd);
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LMCMM, mic, &ad1843_RMCMM, mic);
|
|
|
|
|
|
|
|
return mask;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Setup ad1843 for D/A conversion. */
|
|
|
|
|
|
|
|
static void ad1843_setup_dac(lithium_t *lith,
|
|
|
|
int framerate,
|
|
|
|
int fmt,
|
|
|
|
int channels)
|
|
|
|
{
|
|
|
|
int ad_fmt = 0, ad_mode = 0;
|
|
|
|
|
|
|
|
DBGEV("(lith=0x%p, framerate=%d, fmt=%d, channels=%d)\n",
|
|
|
|
lith, framerate, fmt, channels);
|
|
|
|
|
|
|
|
switch (fmt) {
|
|
|
|
case AFMT_S8: ad_fmt = 1; break;
|
|
|
|
case AFMT_U8: ad_fmt = 1; break;
|
|
|
|
case AFMT_S16_LE: ad_fmt = 1; break;
|
|
|
|
case AFMT_MU_LAW: ad_fmt = 2; break;
|
|
|
|
case AFMT_A_LAW: ad_fmt = 3; break;
|
|
|
|
default: ASSERT(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (channels) {
|
|
|
|
case 2: ad_mode = 0; break;
|
|
|
|
case 1: ad_mode = 1; break;
|
|
|
|
default: ASSERT(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
DBGPV("ad_mode = %d, ad_fmt = %d\n", ad_mode, ad_fmt);
|
|
|
|
ASSERT(framerate >= 4000 && framerate <= 49000);
|
|
|
|
ad1843_write_bits(lith, &ad1843_C1C, framerate);
|
|
|
|
ad1843_write_multi(lith, 2,
|
|
|
|
&ad1843_DA1SM, ad_mode, &ad1843_DA1F, ad_fmt);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ad1843_shutdown_dac(lithium_t *lith)
|
|
|
|
{
|
|
|
|
ad1843_write_bits(lith, &ad1843_DA1F, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ad1843_setup_adc(lithium_t *lith, int framerate, int fmt, int channels)
|
|
|
|
{
|
|
|
|
int da_fmt = 0;
|
|
|
|
|
|
|
|
DBGEV("(lith=0x%p, framerate=%d, fmt=%d, channels=%d)\n",
|
|
|
|
lith, framerate, fmt, channels);
|
|
|
|
|
|
|
|
switch (fmt) {
|
|
|
|
case AFMT_S8: da_fmt = 1; break;
|
|
|
|
case AFMT_U8: da_fmt = 1; break;
|
|
|
|
case AFMT_S16_LE: da_fmt = 1; break;
|
|
|
|
case AFMT_MU_LAW: da_fmt = 2; break;
|
|
|
|
case AFMT_A_LAW: da_fmt = 3; break;
|
|
|
|
default: ASSERT(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
DBGPV("da_fmt = %d\n", da_fmt);
|
|
|
|
ASSERT(framerate >= 4000 && framerate <= 49000);
|
|
|
|
ad1843_write_bits(lith, &ad1843_C2C, framerate);
|
|
|
|
ad1843_write_multi(lith, 2,
|
|
|
|
&ad1843_ADLF, da_fmt, &ad1843_ADRF, da_fmt);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ad1843_shutdown_adc(lithium_t *lith)
|
|
|
|
{
|
|
|
|
/* nothing to do */
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Fully initialize the ad1843. As described in the AD1843 data
|
|
|
|
* sheet, section "START-UP SEQUENCE". The numbered comments are
|
|
|
|
* subsection headings from the data sheet. See the data sheet, pages
|
|
|
|
* 52-54, for more info.
|
|
|
|
*
|
|
|
|
* return 0 on success, -errno on failure. */
|
|
|
|
|
|
|
|
static int __init ad1843_init(lithium_t *lith)
|
|
|
|
{
|
|
|
|
unsigned long later;
|
|
|
|
int err;
|
|
|
|
|
|
|
|
err = li_init(lith);
|
|
|
|
if (err)
|
|
|
|
return err;
|
|
|
|
|
|
|
|
if (ad1843_read_bits(lith, &ad1843_INIT) != 0) {
|
|
|
|
printk(KERN_ERR "vwsnd sound: AD1843 won't initialize\n");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
|
|
|
|
ad1843_write_bits(lith, &ad1843_SCF, 1);
|
|
|
|
|
|
|
|
/* 4. Put the conversion resources into standby. */
|
|
|
|
|
|
|
|
ad1843_write_bits(lith, &ad1843_PDNI, 0);
|
|
|
|
later = jiffies + HZ / 2; /* roughly half a second */
|
|
|
|
DBGDO(shut_up++);
|
|
|
|
while (ad1843_read_bits(lith, &ad1843_PDNO)) {
|
|
|
|
if (time_after(jiffies, later)) {
|
|
|
|
printk(KERN_ERR
|
|
|
|
"vwsnd audio: AD1843 won't power up\n");
|
|
|
|
return -EIO;
|
|
|
|
}
|
|
|
|
schedule();
|
|
|
|
}
|
|
|
|
DBGDO(shut_up--);
|
|
|
|
|
|
|
|
/* 5. Power up the clock generators and enable clock output pins. */
|
|
|
|
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_C1EN, 1, &ad1843_C2EN, 1);
|
|
|
|
|
|
|
|
/* 6. Configure conversion resources while they are in standby. */
|
|
|
|
|
|
|
|
/* DAC1 uses clock 1 as source, ADC uses clock 2. Always. */
|
|
|
|
|
|
|
|
ad1843_write_multi(lith, 3,
|
|
|
|
&ad1843_DA1C, 1,
|
|
|
|
&ad1843_ADLC, 2,
|
|
|
|
&ad1843_ADRC, 2);
|
|
|
|
|
|
|
|
/* 7. Enable conversion resources. */
|
|
|
|
|
|
|
|
ad1843_write_bits(lith, &ad1843_ADTLK, 1);
|
|
|
|
ad1843_write_multi(lith, 5,
|
|
|
|
&ad1843_ANAEN, 1,
|
|
|
|
&ad1843_AAMEN, 1,
|
|
|
|
&ad1843_DA1EN, 1,
|
|
|
|
&ad1843_ADLEN, 1,
|
|
|
|
&ad1843_ADREN, 1);
|
|
|
|
|
|
|
|
/* 8. Configure conversion resources while they are enabled. */
|
|
|
|
|
|
|
|
ad1843_write_bits(lith, &ad1843_DA1C, 1);
|
|
|
|
|
|
|
|
/* Unmute all channels. */
|
|
|
|
|
|
|
|
ad1843_set_outsrc(lith,
|
|
|
|
(SOUND_MASK_PCM | SOUND_MASK_LINE |
|
|
|
|
SOUND_MASK_MIC | SOUND_MASK_CD));
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LDA1AM, 0, &ad1843_RDA1AM, 0);
|
|
|
|
|
|
|
|
/* Set default recording source to Line In and set
|
|
|
|
* mic gain to +20 dB.
|
|
|
|
*/
|
|
|
|
|
|
|
|
ad1843_set_recsrc(lith, SOUND_MASK_LINE);
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_LMGE, 1, &ad1843_RMGE, 1);
|
|
|
|
|
|
|
|
/* Set Speaker Out level to +/- 4V and unmute it. */
|
|
|
|
|
|
|
|
ad1843_write_multi(lith, 2, &ad1843_HPOS, 1, &ad1843_HPOM, 0);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* PCM I/O */
|
|
|
|
|
|
|
|
#define READ_INTR_MASK (LI_INTR_COMM1_TRIG | LI_INTR_COMM1_OVERFLOW)
|
|
|
|
#define WRITE_INTR_MASK (LI_INTR_COMM2_TRIG | LI_INTR_COMM2_UNDERFLOW)
|
|
|
|
|
|
|
|
typedef enum vwsnd_port_swstate { /* software state */
|
|
|
|
SW_OFF,
|
|
|
|
SW_INITIAL,
|
|
|
|
SW_RUN,
|
|
|
|
SW_DRAIN,
|
|
|
|
} vwsnd_port_swstate_t;
|
|
|
|
|
|
|
|
typedef enum vwsnd_port_hwstate { /* hardware state */
|
|
|
|
HW_STOPPED,
|
|
|
|
HW_RUNNING,
|
|
|
|
} vwsnd_port_hwstate_t;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* These flags are read by ISR, but only written at baseline.
|
|
|
|
*/
|
|
|
|
|
|
|
|
typedef enum vwsnd_port_flags {
|
|
|
|
DISABLED = 1 << 0,
|
|
|
|
ERFLOWN = 1 << 1, /* overflown or underflown */
|
|
|
|
HW_BUSY = 1 << 2,
|
|
|
|
} vwsnd_port_flags_t;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* vwsnd_port is the per-port data structure. Each device has two
|
|
|
|
* ports, one for input and one for output.
|
|
|
|
*
|
|
|
|
* Locking:
|
|
|
|
*
|
|
|
|
* port->lock protects: hwstate, flags, swb_[iu]_avail.
|
|
|
|
*
|
2006-03-23 19:00:39 +08:00
|
|
|
* devc->io_mutex protects: swstate, sw_*, swb_[iu]_idx.
|
2005-04-17 06:20:36 +08:00
|
|
|
*
|
|
|
|
* everything else is only written by open/release or
|
|
|
|
* pcm_{setup,shutdown}(), which are serialized by a
|
2006-03-23 19:00:39 +08:00
|
|
|
* combination of devc->open_mutex and devc->io_mutex.
|
2005-04-17 06:20:36 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
typedef struct vwsnd_port {
|
|
|
|
|
|
|
|
spinlock_t lock;
|
|
|
|
wait_queue_head_t queue;
|
|
|
|
vwsnd_port_swstate_t swstate;
|
|
|
|
vwsnd_port_hwstate_t hwstate;
|
|
|
|
vwsnd_port_flags_t flags;
|
|
|
|
|
|
|
|
int sw_channels;
|
|
|
|
int sw_samplefmt;
|
|
|
|
int sw_framerate;
|
|
|
|
int sample_size;
|
|
|
|
int frame_size;
|
|
|
|
unsigned int zero_word; /* zero for the sample format */
|
|
|
|
|
|
|
|
int sw_fragshift;
|
|
|
|
int sw_fragcount;
|
|
|
|
int sw_subdivshift;
|
|
|
|
|
|
|
|
unsigned int hw_fragshift;
|
|
|
|
unsigned int hw_fragsize;
|
|
|
|
unsigned int hw_fragcount;
|
|
|
|
|
|
|
|
int hwbuf_size;
|
|
|
|
unsigned long hwbuf_paddr;
|
|
|
|
unsigned long hwbuf_vaddr;
|
|
|
|
void * hwbuf; /* hwbuf == hwbuf_vaddr */
|
|
|
|
int hwbuf_max; /* max bytes to preload */
|
|
|
|
|
|
|
|
void * swbuf;
|
|
|
|
unsigned int swbuf_size; /* size in bytes */
|
|
|
|
unsigned int swb_u_idx; /* index of next user byte */
|
|
|
|
unsigned int swb_i_idx; /* index of next intr byte */
|
|
|
|
unsigned int swb_u_avail; /* # bytes avail to user */
|
|
|
|
unsigned int swb_i_avail; /* # bytes avail to intr */
|
|
|
|
|
|
|
|
dma_chan_t chan;
|
|
|
|
|
|
|
|
/* Accounting */
|
|
|
|
|
|
|
|
int byte_count;
|
|
|
|
int frag_count;
|
|
|
|
int MSC_offset;
|
|
|
|
|
|
|
|
} vwsnd_port_t;
|
|
|
|
|
|
|
|
/* vwsnd_dev is the per-device data structure. */
|
|
|
|
|
|
|
|
typedef struct vwsnd_dev {
|
|
|
|
struct vwsnd_dev *next_dev;
|
|
|
|
int audio_minor; /* minor number of audio device */
|
|
|
|
int mixer_minor; /* minor number of mixer device */
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
struct mutex open_mutex;
|
|
|
|
struct mutex io_mutex;
|
|
|
|
struct mutex mix_mutex;
|
2008-09-03 03:28:45 +08:00
|
|
|
fmode_t open_mode;
|
2005-04-17 06:20:36 +08:00
|
|
|
wait_queue_head_t open_wait;
|
|
|
|
|
|
|
|
lithium_t lith;
|
|
|
|
|
|
|
|
vwsnd_port_t rport;
|
|
|
|
vwsnd_port_t wport;
|
|
|
|
} vwsnd_dev_t;
|
|
|
|
|
|
|
|
static vwsnd_dev_t *vwsnd_dev_list; /* linked list of all devices */
|
|
|
|
|
|
|
|
static atomic_t vwsnd_use_count = ATOMIC_INIT(0);
|
|
|
|
|
|
|
|
# define INC_USE_COUNT (atomic_inc(&vwsnd_use_count))
|
|
|
|
# define DEC_USE_COUNT (atomic_dec(&vwsnd_use_count))
|
|
|
|
# define IN_USE (atomic_read(&vwsnd_use_count) != 0)
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Lithium can only DMA multiples of 32 bytes. Its DMA buffer may
|
|
|
|
* be up to 8 Kb. This driver always uses 8 Kb.
|
|
|
|
*
|
|
|
|
* Memory bug workaround -- I'm not sure what's going on here, but
|
|
|
|
* somehow pcm_copy_out() was triggering segv's going on to the next
|
|
|
|
* page of the hw buffer. So, I make the hw buffer one size bigger
|
|
|
|
* than we actually use. That way, the following page is allocated
|
|
|
|
* and mapped, and no error. I suspect that something is broken
|
|
|
|
* in Cobalt, but haven't really investigated. HBO is the actual
|
|
|
|
* size of the buffer, and HWBUF_ORDER is what we allocate.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define HWBUF_SHIFT 13
|
|
|
|
#define HWBUF_SIZE (1 << HWBUF_SHIFT)
|
|
|
|
# define HBO (HWBUF_SHIFT > PAGE_SHIFT ? HWBUF_SHIFT - PAGE_SHIFT : 0)
|
|
|
|
# define HWBUF_ORDER (HBO + 1) /* next size bigger */
|
|
|
|
#define MIN_SPEED 4000
|
|
|
|
#define MAX_SPEED 49000
|
|
|
|
|
|
|
|
#define MIN_FRAGSHIFT (DMACHUNK_SHIFT + 1)
|
|
|
|
#define MAX_FRAGSHIFT (PAGE_SHIFT)
|
|
|
|
#define MIN_FRAGSIZE (1 << MIN_FRAGSHIFT)
|
|
|
|
#define MAX_FRAGSIZE (1 << MAX_FRAGSHIFT)
|
|
|
|
#define MIN_FRAGCOUNT(fragsize) 3
|
|
|
|
#define MAX_FRAGCOUNT(fragsize) (32 * PAGE_SIZE / (fragsize))
|
|
|
|
#define DEFAULT_FRAGSHIFT 12
|
|
|
|
#define DEFAULT_FRAGCOUNT 16
|
|
|
|
#define DEFAULT_SUBDIVSHIFT 0
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The software buffer (swbuf) is a ring buffer shared between user
|
|
|
|
* level and interrupt level. Each level owns some of the bytes in
|
|
|
|
* the buffer, and may give bytes away by calling swb_inc_{u,i}().
|
|
|
|
* User level calls _u for user, and interrupt level calls _i for
|
|
|
|
* interrupt.
|
|
|
|
*
|
|
|
|
* port->swb_{u,i}_avail is the number of bytes available to that level.
|
|
|
|
*
|
|
|
|
* port->swb_{u,i}_idx is the index of the first available byte in the
|
|
|
|
* buffer.
|
|
|
|
*
|
|
|
|
* Each level calls swb_inc_{u,i}() to atomically increment its index,
|
|
|
|
* recalculate the number of bytes available for both sides, and
|
|
|
|
* return the number of bytes available. Since each side can only
|
|
|
|
* give away bytes, the other side can only increase the number of
|
|
|
|
* bytes available to this side. Each side updates its own index
|
|
|
|
* variable, swb_{u,i}_idx, so no lock is needed to read it.
|
|
|
|
*
|
|
|
|
* To query the number of bytes available, call swb_inc_{u,i} with an
|
|
|
|
* increment of zero.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static __inline__ unsigned int __swb_inc_u(vwsnd_port_t *port, int inc)
|
|
|
|
{
|
|
|
|
if (inc) {
|
|
|
|
port->swb_u_idx += inc;
|
|
|
|
port->swb_u_idx %= port->swbuf_size;
|
|
|
|
port->swb_u_avail -= inc;
|
|
|
|
port->swb_i_avail += inc;
|
|
|
|
}
|
|
|
|
return port->swb_u_avail;
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ unsigned int swb_inc_u(vwsnd_port_t *port, int inc)
|
|
|
|
{
|
|
|
|
unsigned long flags;
|
|
|
|
unsigned int ret;
|
|
|
|
|
|
|
|
spin_lock_irqsave(&port->lock, flags);
|
|
|
|
{
|
|
|
|
ret = __swb_inc_u(port, inc);
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&port->lock, flags);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ unsigned int __swb_inc_i(vwsnd_port_t *port, int inc)
|
|
|
|
{
|
|
|
|
if (inc) {
|
|
|
|
port->swb_i_idx += inc;
|
|
|
|
port->swb_i_idx %= port->swbuf_size;
|
|
|
|
port->swb_i_avail -= inc;
|
|
|
|
port->swb_u_avail += inc;
|
|
|
|
}
|
|
|
|
return port->swb_i_avail;
|
|
|
|
}
|
|
|
|
|
|
|
|
static __inline__ unsigned int swb_inc_i(vwsnd_port_t *port, int inc)
|
|
|
|
{
|
|
|
|
unsigned long flags;
|
|
|
|
unsigned int ret;
|
|
|
|
|
|
|
|
spin_lock_irqsave(&port->lock, flags);
|
|
|
|
{
|
|
|
|
ret = __swb_inc_i(port, inc);
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&port->lock, flags);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* pcm_setup - this routine initializes all port state after
|
|
|
|
* mode-setting ioctls have been done, but before the first I/O is
|
|
|
|
* done.
|
|
|
|
*
|
2006-03-23 19:00:39 +08:00
|
|
|
* Locking: called with devc->io_mutex held.
|
2005-04-17 06:20:36 +08:00
|
|
|
*
|
|
|
|
* Returns 0 on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int pcm_setup(vwsnd_dev_t *devc,
|
|
|
|
vwsnd_port_t *rport,
|
|
|
|
vwsnd_port_t *wport)
|
|
|
|
{
|
|
|
|
vwsnd_port_t *aport = rport ? rport : wport;
|
|
|
|
int sample_size;
|
|
|
|
unsigned int zero_word;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p, rport=0x%p, wport=0x%p)\n", devc, rport, wport);
|
|
|
|
|
|
|
|
ASSERT(aport != NULL);
|
|
|
|
if (aport->swbuf != NULL)
|
|
|
|
return 0;
|
|
|
|
switch (aport->sw_samplefmt) {
|
|
|
|
case AFMT_MU_LAW:
|
|
|
|
sample_size = 1;
|
|
|
|
zero_word = 0xFFFFFFFF ^ 0x80808080;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case AFMT_A_LAW:
|
|
|
|
sample_size = 1;
|
|
|
|
zero_word = 0xD5D5D5D5 ^ 0x80808080;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case AFMT_U8:
|
|
|
|
sample_size = 1;
|
|
|
|
zero_word = 0x80808080;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case AFMT_S8:
|
|
|
|
sample_size = 1;
|
|
|
|
zero_word = 0x00000000;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case AFMT_S16_LE:
|
|
|
|
sample_size = 2;
|
|
|
|
zero_word = 0x00000000;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
sample_size = 0; /* prevent compiler warning */
|
|
|
|
zero_word = 0;
|
|
|
|
ASSERT(0);
|
|
|
|
}
|
|
|
|
aport->sample_size = sample_size;
|
|
|
|
aport->zero_word = zero_word;
|
|
|
|
aport->frame_size = aport->sw_channels * aport->sample_size;
|
|
|
|
aport->hw_fragshift = aport->sw_fragshift - aport->sw_subdivshift;
|
|
|
|
aport->hw_fragsize = 1 << aport->hw_fragshift;
|
|
|
|
aport->hw_fragcount = aport->sw_fragcount << aport->sw_subdivshift;
|
|
|
|
ASSERT(aport->hw_fragsize >= MIN_FRAGSIZE);
|
|
|
|
ASSERT(aport->hw_fragsize <= MAX_FRAGSIZE);
|
|
|
|
ASSERT(aport->hw_fragcount >= MIN_FRAGCOUNT(aport->hw_fragsize));
|
|
|
|
ASSERT(aport->hw_fragcount <= MAX_FRAGCOUNT(aport->hw_fragsize));
|
|
|
|
if (rport) {
|
|
|
|
int hwfrags, swfrags;
|
|
|
|
rport->hwbuf_max = aport->hwbuf_size - DMACHUNK_SIZE;
|
|
|
|
hwfrags = rport->hwbuf_max >> aport->hw_fragshift;
|
|
|
|
swfrags = aport->hw_fragcount - hwfrags;
|
|
|
|
if (swfrags < 2)
|
|
|
|
swfrags = 2;
|
|
|
|
rport->swbuf_size = swfrags * aport->hw_fragsize;
|
|
|
|
DBGPV("hwfrags = %d, swfrags = %d\n", hwfrags, swfrags);
|
|
|
|
DBGPV("read hwbuf_max = %d, swbuf_size = %d\n",
|
|
|
|
rport->hwbuf_max, rport->swbuf_size);
|
|
|
|
}
|
|
|
|
if (wport) {
|
|
|
|
int hwfrags, swfrags;
|
|
|
|
int total_bytes = aport->hw_fragcount * aport->hw_fragsize;
|
|
|
|
wport->hwbuf_max = aport->hwbuf_size - DMACHUNK_SIZE;
|
|
|
|
if (wport->hwbuf_max > total_bytes)
|
|
|
|
wport->hwbuf_max = total_bytes;
|
|
|
|
hwfrags = wport->hwbuf_max >> aport->hw_fragshift;
|
|
|
|
DBGPV("hwfrags = %d\n", hwfrags);
|
|
|
|
swfrags = aport->hw_fragcount - hwfrags;
|
|
|
|
if (swfrags < 2)
|
|
|
|
swfrags = 2;
|
|
|
|
wport->swbuf_size = swfrags * aport->hw_fragsize;
|
|
|
|
DBGPV("hwfrags = %d, swfrags = %d\n", hwfrags, swfrags);
|
|
|
|
DBGPV("write hwbuf_max = %d, swbuf_size = %d\n",
|
|
|
|
wport->hwbuf_max, wport->swbuf_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
aport->swb_u_idx = 0;
|
|
|
|
aport->swb_i_idx = 0;
|
|
|
|
aport->byte_count = 0;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Is this a Cobalt bug? We need to make this buffer extend
|
|
|
|
* one page further than we actually use -- somehow memcpy
|
|
|
|
* causes an exceptoin otherwise. I suspect there's a bug in
|
|
|
|
* Cobalt (or somewhere) where it's generating a fault on a
|
|
|
|
* speculative load or something. Obviously, I haven't taken
|
|
|
|
* the time to track it down.
|
|
|
|
*/
|
|
|
|
|
|
|
|
aport->swbuf = vmalloc(aport->swbuf_size + PAGE_SIZE);
|
|
|
|
if (!aport->swbuf)
|
|
|
|
return -ENOMEM;
|
|
|
|
if (rport && wport) {
|
|
|
|
ASSERT(aport == rport);
|
|
|
|
ASSERT(wport->swbuf == NULL);
|
|
|
|
/* One extra page - see comment above. */
|
|
|
|
wport->swbuf = vmalloc(aport->swbuf_size + PAGE_SIZE);
|
|
|
|
if (!wport->swbuf) {
|
|
|
|
vfree(aport->swbuf);
|
|
|
|
aport->swbuf = NULL;
|
|
|
|
return -ENOMEM;
|
|
|
|
}
|
|
|
|
wport->sample_size = rport->sample_size;
|
|
|
|
wport->zero_word = rport->zero_word;
|
|
|
|
wport->frame_size = rport->frame_size;
|
|
|
|
wport->hw_fragshift = rport->hw_fragshift;
|
|
|
|
wport->hw_fragsize = rport->hw_fragsize;
|
|
|
|
wport->hw_fragcount = rport->hw_fragcount;
|
|
|
|
wport->swbuf_size = rport->swbuf_size;
|
|
|
|
wport->hwbuf_max = rport->hwbuf_max;
|
|
|
|
wport->swb_u_idx = rport->swb_u_idx;
|
|
|
|
wport->swb_i_idx = rport->swb_i_idx;
|
|
|
|
wport->byte_count = rport->byte_count;
|
|
|
|
}
|
|
|
|
if (rport) {
|
|
|
|
rport->swb_u_avail = 0;
|
|
|
|
rport->swb_i_avail = rport->swbuf_size;
|
|
|
|
rport->swstate = SW_RUN;
|
|
|
|
li_setup_dma(&rport->chan,
|
|
|
|
&li_comm1,
|
|
|
|
&devc->lith,
|
|
|
|
rport->hwbuf_paddr,
|
|
|
|
HWBUF_SHIFT,
|
|
|
|
rport->hw_fragshift,
|
|
|
|
rport->sw_channels,
|
|
|
|
rport->sample_size);
|
|
|
|
ad1843_setup_adc(&devc->lith,
|
|
|
|
rport->sw_framerate,
|
|
|
|
rport->sw_samplefmt,
|
|
|
|
rport->sw_channels);
|
|
|
|
li_enable_interrupts(&devc->lith, READ_INTR_MASK);
|
|
|
|
if (!(rport->flags & DISABLED)) {
|
|
|
|
ustmsc_t ustmsc;
|
|
|
|
rport->hwstate = HW_RUNNING;
|
|
|
|
li_activate_dma(&rport->chan);
|
|
|
|
li_read_USTMSC(&rport->chan, &ustmsc);
|
|
|
|
rport->MSC_offset = ustmsc.msc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (wport) {
|
|
|
|
if (wport->hwbuf_max > wport->swbuf_size)
|
|
|
|
wport->hwbuf_max = wport->swbuf_size;
|
|
|
|
wport->flags &= ~ERFLOWN;
|
|
|
|
wport->swb_u_avail = wport->swbuf_size;
|
|
|
|
wport->swb_i_avail = 0;
|
|
|
|
wport->swstate = SW_RUN;
|
|
|
|
li_setup_dma(&wport->chan,
|
|
|
|
&li_comm2,
|
|
|
|
&devc->lith,
|
|
|
|
wport->hwbuf_paddr,
|
|
|
|
HWBUF_SHIFT,
|
|
|
|
wport->hw_fragshift,
|
|
|
|
wport->sw_channels,
|
|
|
|
wport->sample_size);
|
|
|
|
ad1843_setup_dac(&devc->lith,
|
|
|
|
wport->sw_framerate,
|
|
|
|
wport->sw_samplefmt,
|
|
|
|
wport->sw_channels);
|
|
|
|
li_enable_interrupts(&devc->lith, WRITE_INTR_MASK);
|
|
|
|
}
|
|
|
|
DBGRV();
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* pcm_shutdown_port - shut down one port (direction) for PCM I/O.
|
|
|
|
* Only called from pcm_shutdown.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void pcm_shutdown_port(vwsnd_dev_t *devc,
|
|
|
|
vwsnd_port_t *aport,
|
|
|
|
unsigned int mask)
|
|
|
|
{
|
|
|
|
unsigned long flags;
|
|
|
|
vwsnd_port_hwstate_t hwstate;
|
|
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
|
|
|
|
|
|
aport->swstate = SW_INITIAL;
|
|
|
|
add_wait_queue(&aport->queue, &wait);
|
|
|
|
while (1) {
|
|
|
|
set_current_state(TASK_UNINTERRUPTIBLE);
|
|
|
|
spin_lock_irqsave(&aport->lock, flags);
|
|
|
|
{
|
|
|
|
hwstate = aport->hwstate;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&aport->lock, flags);
|
|
|
|
if (hwstate == HW_STOPPED)
|
|
|
|
break;
|
|
|
|
schedule();
|
|
|
|
}
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&aport->queue, &wait);
|
|
|
|
li_disable_interrupts(&devc->lith, mask);
|
|
|
|
if (aport == &devc->rport)
|
|
|
|
ad1843_shutdown_adc(&devc->lith);
|
|
|
|
else /* aport == &devc->wport) */
|
|
|
|
ad1843_shutdown_dac(&devc->lith);
|
|
|
|
li_shutdown_dma(&aport->chan);
|
|
|
|
vfree(aport->swbuf);
|
|
|
|
aport->swbuf = NULL;
|
|
|
|
aport->byte_count = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* pcm_shutdown undoes what pcm_setup did.
|
|
|
|
* Also sets the ports' swstate to newstate.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void pcm_shutdown(vwsnd_dev_t *devc,
|
|
|
|
vwsnd_port_t *rport,
|
|
|
|
vwsnd_port_t *wport)
|
|
|
|
{
|
|
|
|
DBGEV("(devc=0x%p, rport=0x%p, wport=0x%p)\n", devc, rport, wport);
|
|
|
|
|
|
|
|
if (rport && rport->swbuf) {
|
|
|
|
DBGPV("shutting down rport\n");
|
|
|
|
pcm_shutdown_port(devc, rport, READ_INTR_MASK);
|
|
|
|
}
|
|
|
|
if (wport && wport->swbuf) {
|
|
|
|
DBGPV("shutting down wport\n");
|
|
|
|
pcm_shutdown_port(devc, wport, WRITE_INTR_MASK);
|
|
|
|
}
|
|
|
|
DBGRV();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pcm_copy_in(vwsnd_port_t *rport, int swidx, int hwidx, int nb)
|
|
|
|
{
|
|
|
|
char *src = rport->hwbuf + hwidx;
|
|
|
|
char *dst = rport->swbuf + swidx;
|
|
|
|
int fmt = rport->sw_samplefmt;
|
|
|
|
|
|
|
|
DBGPV("swidx = %d, hwidx = %d\n", swidx, hwidx);
|
|
|
|
ASSERT(rport->hwbuf != NULL);
|
|
|
|
ASSERT(rport->swbuf != NULL);
|
|
|
|
ASSERT(nb > 0 && (nb % 32) == 0);
|
|
|
|
ASSERT(swidx % 32 == 0 && hwidx % 32 == 0);
|
|
|
|
ASSERT(swidx >= 0 && swidx + nb <= rport->swbuf_size);
|
|
|
|
ASSERT(hwidx >= 0 && hwidx + nb <= rport->hwbuf_size);
|
|
|
|
|
|
|
|
if (fmt == AFMT_MU_LAW || fmt == AFMT_A_LAW || fmt == AFMT_S8) {
|
|
|
|
|
|
|
|
/* See Sample Format Notes above. */
|
|
|
|
|
|
|
|
char *end = src + nb;
|
|
|
|
while (src < end)
|
|
|
|
*dst++ = *src++ ^ 0x80;
|
|
|
|
} else
|
|
|
|
memcpy(dst, src, nb);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void pcm_copy_out(vwsnd_port_t *wport, int swidx, int hwidx, int nb)
|
|
|
|
{
|
|
|
|
char *src = wport->swbuf + swidx;
|
|
|
|
char *dst = wport->hwbuf + hwidx;
|
|
|
|
int fmt = wport->sw_samplefmt;
|
|
|
|
|
|
|
|
ASSERT(nb > 0 && (nb % 32) == 0);
|
|
|
|
ASSERT(wport->hwbuf != NULL);
|
|
|
|
ASSERT(wport->swbuf != NULL);
|
|
|
|
ASSERT(swidx % 32 == 0 && hwidx % 32 == 0);
|
|
|
|
ASSERT(swidx >= 0 && swidx + nb <= wport->swbuf_size);
|
|
|
|
ASSERT(hwidx >= 0 && hwidx + nb <= wport->hwbuf_size);
|
|
|
|
if (fmt == AFMT_MU_LAW || fmt == AFMT_A_LAW || fmt == AFMT_S8) {
|
|
|
|
|
|
|
|
/* See Sample Format Notes above. */
|
|
|
|
|
|
|
|
char *end = src + nb;
|
|
|
|
while (src < end)
|
|
|
|
*dst++ = *src++ ^ 0x80;
|
|
|
|
} else
|
|
|
|
memcpy(dst, src, nb);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* pcm_output() is called both from baselevel and from interrupt level.
|
|
|
|
* This is where audio frames are copied into the hardware-accessible
|
|
|
|
* ring buffer.
|
|
|
|
*
|
|
|
|
* Locking note: The part of this routine that figures out what to do
|
|
|
|
* holds wport->lock. The longer part releases wport->lock, but sets
|
|
|
|
* wport->flags & HW_BUSY. Afterward, it reacquires wport->lock, and
|
|
|
|
* checks for more work to do.
|
|
|
|
*
|
|
|
|
* If another thread calls pcm_output() while HW_BUSY is set, it
|
|
|
|
* returns immediately, knowing that the thread that set HW_BUSY will
|
|
|
|
* look for more work to do before returning.
|
|
|
|
*
|
|
|
|
* This has the advantage that port->lock is held for several short
|
|
|
|
* periods instead of one long period. Also, when pcm_output is
|
|
|
|
* called from base level, it reenables interrupts.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void pcm_output(vwsnd_dev_t *devc, int erflown, int nb)
|
|
|
|
{
|
|
|
|
vwsnd_port_t *wport = &devc->wport;
|
|
|
|
const int hwmax = wport->hwbuf_max;
|
|
|
|
const int hwsize = wport->hwbuf_size;
|
|
|
|
const int swsize = wport->swbuf_size;
|
|
|
|
const int fragsize = wport->hw_fragsize;
|
|
|
|
unsigned long iflags;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p, erflown=%d, nb=%d)\n", devc, erflown, nb);
|
|
|
|
spin_lock_irqsave(&wport->lock, iflags);
|
|
|
|
if (erflown)
|
|
|
|
wport->flags |= ERFLOWN;
|
|
|
|
(void) __swb_inc_u(wport, nb);
|
|
|
|
if (wport->flags & HW_BUSY) {
|
|
|
|
spin_unlock_irqrestore(&wport->lock, iflags);
|
|
|
|
DBGPV("returning: HW BUSY\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (wport->flags & DISABLED) {
|
|
|
|
spin_unlock_irqrestore(&wport->lock, iflags);
|
|
|
|
DBGPV("returning: DISABLED\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
wport->flags |= HW_BUSY;
|
|
|
|
while (1) {
|
|
|
|
int swptr, hwptr, hw_avail, sw_avail, swidx;
|
|
|
|
vwsnd_port_hwstate_t hwstate = wport->hwstate;
|
|
|
|
vwsnd_port_swstate_t swstate = wport->swstate;
|
|
|
|
int hw_unavail;
|
|
|
|
ustmsc_t ustmsc;
|
|
|
|
|
|
|
|
hwptr = li_read_hwptr(&wport->chan);
|
|
|
|
swptr = li_read_swptr(&wport->chan);
|
|
|
|
hw_unavail = (swptr - hwptr + hwsize) % hwsize;
|
|
|
|
hw_avail = (hwmax - hw_unavail) & -fragsize;
|
|
|
|
sw_avail = wport->swb_i_avail & -fragsize;
|
|
|
|
if (sw_avail && swstate == SW_RUN) {
|
|
|
|
if (wport->flags & ERFLOWN) {
|
|
|
|
wport->flags &= ~ERFLOWN;
|
|
|
|
}
|
|
|
|
} else if (swstate == SW_INITIAL ||
|
|
|
|
swstate == SW_OFF ||
|
|
|
|
(swstate == SW_DRAIN &&
|
|
|
|
!sw_avail &&
|
|
|
|
(wport->flags & ERFLOWN))) {
|
|
|
|
DBGP("stopping. hwstate = %d\n", hwstate);
|
|
|
|
if (hwstate != HW_STOPPED) {
|
|
|
|
li_deactivate_dma(&wport->chan);
|
|
|
|
wport->hwstate = HW_STOPPED;
|
|
|
|
}
|
|
|
|
wake_up(&wport->queue);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!sw_avail || !hw_avail)
|
|
|
|
break;
|
|
|
|
spin_unlock_irqrestore(&wport->lock, iflags);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We gave up the port lock, but we have the HW_BUSY flag.
|
|
|
|
* Proceed without accessing any nonlocal state.
|
|
|
|
* Do not exit the loop -- must check for more work.
|
|
|
|
*/
|
|
|
|
|
|
|
|
swidx = wport->swb_i_idx;
|
|
|
|
nb = hw_avail;
|
|
|
|
if (nb > sw_avail)
|
|
|
|
nb = sw_avail;
|
|
|
|
if (nb > hwsize - swptr)
|
|
|
|
nb = hwsize - swptr; /* don't overflow hwbuf */
|
|
|
|
if (nb > swsize - swidx)
|
|
|
|
nb = swsize - swidx; /* don't overflow swbuf */
|
|
|
|
ASSERT(nb > 0);
|
|
|
|
if (nb % fragsize) {
|
|
|
|
DBGP("nb = %d, fragsize = %d\n", nb, fragsize);
|
|
|
|
DBGP("hw_avail = %d\n", hw_avail);
|
|
|
|
DBGP("sw_avail = %d\n", sw_avail);
|
|
|
|
DBGP("hwsize = %d, swptr = %d\n", hwsize, swptr);
|
|
|
|
DBGP("swsize = %d, swidx = %d\n", swsize, swidx);
|
|
|
|
}
|
|
|
|
ASSERT(!(nb % fragsize));
|
|
|
|
DBGPV("copying swb[%d..%d] to hwb[%d..%d]\n",
|
|
|
|
swidx, swidx + nb, swptr, swptr + nb);
|
|
|
|
pcm_copy_out(wport, swidx, swptr, nb);
|
|
|
|
li_write_swptr(&wport->chan, (swptr + nb) % hwsize);
|
|
|
|
spin_lock_irqsave(&wport->lock, iflags);
|
|
|
|
if (hwstate == HW_STOPPED) {
|
|
|
|
DBGPV("starting\n");
|
|
|
|
li_activate_dma(&wport->chan);
|
|
|
|
wport->hwstate = HW_RUNNING;
|
|
|
|
li_read_USTMSC(&wport->chan, &ustmsc);
|
|
|
|
ASSERT(wport->byte_count % wport->frame_size == 0);
|
|
|
|
wport->MSC_offset = ustmsc.msc - wport->byte_count / wport->frame_size;
|
|
|
|
}
|
|
|
|
__swb_inc_i(wport, nb);
|
|
|
|
wport->byte_count += nb;
|
|
|
|
wport->frag_count += nb / fragsize;
|
|
|
|
ASSERT(nb % fragsize == 0);
|
|
|
|
wake_up(&wport->queue);
|
|
|
|
}
|
|
|
|
wport->flags &= ~HW_BUSY;
|
|
|
|
spin_unlock_irqrestore(&wport->lock, iflags);
|
|
|
|
DBGRV();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* pcm_input() is called both from baselevel and from interrupt level.
|
|
|
|
* This is where audio frames are copied out of the hardware-accessible
|
|
|
|
* ring buffer.
|
|
|
|
*
|
|
|
|
* Locking note: The part of this routine that figures out what to do
|
|
|
|
* holds rport->lock. The longer part releases rport->lock, but sets
|
|
|
|
* rport->flags & HW_BUSY. Afterward, it reacquires rport->lock, and
|
|
|
|
* checks for more work to do.
|
|
|
|
*
|
|
|
|
* If another thread calls pcm_input() while HW_BUSY is set, it
|
|
|
|
* returns immediately, knowing that the thread that set HW_BUSY will
|
|
|
|
* look for more work to do before returning.
|
|
|
|
*
|
|
|
|
* This has the advantage that port->lock is held for several short
|
|
|
|
* periods instead of one long period. Also, when pcm_input is
|
|
|
|
* called from base level, it reenables interrupts.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void pcm_input(vwsnd_dev_t *devc, int erflown, int nb)
|
|
|
|
{
|
|
|
|
vwsnd_port_t *rport = &devc->rport;
|
|
|
|
const int hwmax = rport->hwbuf_max;
|
|
|
|
const int hwsize = rport->hwbuf_size;
|
|
|
|
const int swsize = rport->swbuf_size;
|
|
|
|
const int fragsize = rport->hw_fragsize;
|
|
|
|
unsigned long iflags;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p, erflown=%d, nb=%d)\n", devc, erflown, nb);
|
|
|
|
|
|
|
|
spin_lock_irqsave(&rport->lock, iflags);
|
|
|
|
if (erflown)
|
|
|
|
rport->flags |= ERFLOWN;
|
|
|
|
(void) __swb_inc_u(rport, nb);
|
|
|
|
if (rport->flags & HW_BUSY || !rport->swbuf) {
|
|
|
|
spin_unlock_irqrestore(&rport->lock, iflags);
|
|
|
|
DBGPV("returning: HW BUSY or !swbuf\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (rport->flags & DISABLED) {
|
|
|
|
spin_unlock_irqrestore(&rport->lock, iflags);
|
|
|
|
DBGPV("returning: DISABLED\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
rport->flags |= HW_BUSY;
|
|
|
|
while (1) {
|
|
|
|
int swptr, hwptr, hw_avail, sw_avail, swidx;
|
|
|
|
vwsnd_port_hwstate_t hwstate = rport->hwstate;
|
|
|
|
vwsnd_port_swstate_t swstate = rport->swstate;
|
|
|
|
|
|
|
|
hwptr = li_read_hwptr(&rport->chan);
|
|
|
|
swptr = li_read_swptr(&rport->chan);
|
|
|
|
hw_avail = (hwptr - swptr + hwsize) % hwsize & -fragsize;
|
|
|
|
if (hw_avail > hwmax)
|
|
|
|
hw_avail = hwmax;
|
|
|
|
sw_avail = rport->swb_i_avail & -fragsize;
|
|
|
|
if (swstate != SW_RUN) {
|
|
|
|
DBGP("stopping. hwstate = %d\n", hwstate);
|
|
|
|
if (hwstate != HW_STOPPED) {
|
|
|
|
li_deactivate_dma(&rport->chan);
|
|
|
|
rport->hwstate = HW_STOPPED;
|
|
|
|
}
|
|
|
|
wake_up(&rport->queue);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (!sw_avail || !hw_avail)
|
|
|
|
break;
|
|
|
|
spin_unlock_irqrestore(&rport->lock, iflags);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We gave up the port lock, but we have the HW_BUSY flag.
|
|
|
|
* Proceed without accessing any nonlocal state.
|
|
|
|
* Do not exit the loop -- must check for more work.
|
|
|
|
*/
|
|
|
|
|
|
|
|
swidx = rport->swb_i_idx;
|
|
|
|
nb = hw_avail;
|
|
|
|
if (nb > sw_avail)
|
|
|
|
nb = sw_avail;
|
|
|
|
if (nb > hwsize - swptr)
|
|
|
|
nb = hwsize - swptr; /* don't overflow hwbuf */
|
|
|
|
if (nb > swsize - swidx)
|
|
|
|
nb = swsize - swidx; /* don't overflow swbuf */
|
|
|
|
ASSERT(nb > 0);
|
|
|
|
if (nb % fragsize) {
|
|
|
|
DBGP("nb = %d, fragsize = %d\n", nb, fragsize);
|
|
|
|
DBGP("hw_avail = %d\n", hw_avail);
|
|
|
|
DBGP("sw_avail = %d\n", sw_avail);
|
|
|
|
DBGP("hwsize = %d, swptr = %d\n", hwsize, swptr);
|
|
|
|
DBGP("swsize = %d, swidx = %d\n", swsize, swidx);
|
|
|
|
}
|
|
|
|
ASSERT(!(nb % fragsize));
|
|
|
|
DBGPV("copying hwb[%d..%d] to swb[%d..%d]\n",
|
|
|
|
swptr, swptr + nb, swidx, swidx + nb);
|
|
|
|
pcm_copy_in(rport, swidx, swptr, nb);
|
|
|
|
li_write_swptr(&rport->chan, (swptr + nb) % hwsize);
|
|
|
|
spin_lock_irqsave(&rport->lock, iflags);
|
|
|
|
__swb_inc_i(rport, nb);
|
|
|
|
rport->byte_count += nb;
|
|
|
|
rport->frag_count += nb / fragsize;
|
|
|
|
ASSERT(nb % fragsize == 0);
|
|
|
|
wake_up(&rport->queue);
|
|
|
|
}
|
|
|
|
rport->flags &= ~HW_BUSY;
|
|
|
|
spin_unlock_irqrestore(&rport->lock, iflags);
|
|
|
|
DBGRV();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* pcm_flush_frag() writes zero samples to fill the current fragment,
|
|
|
|
* then flushes it to the hardware.
|
|
|
|
*
|
|
|
|
* It is only meaningful to flush output, not input.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void pcm_flush_frag(vwsnd_dev_t *devc)
|
|
|
|
{
|
|
|
|
vwsnd_port_t *wport = &devc->wport;
|
|
|
|
|
|
|
|
DBGPV("swstate = %d\n", wport->swstate);
|
|
|
|
if (wport->swstate == SW_RUN) {
|
|
|
|
int idx = wport->swb_u_idx;
|
|
|
|
int end = (idx + wport->hw_fragsize - 1)
|
|
|
|
>> wport->hw_fragshift
|
|
|
|
<< wport->hw_fragshift;
|
|
|
|
int nb = end - idx;
|
|
|
|
DBGPV("clearing %d bytes\n", nb);
|
|
|
|
if (nb)
|
|
|
|
memset(wport->swbuf + idx,
|
|
|
|
(char) wport->zero_word,
|
|
|
|
nb);
|
|
|
|
wport->swstate = SW_DRAIN;
|
|
|
|
pcm_output(devc, 0, nb);
|
|
|
|
}
|
|
|
|
DBGRV();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Wait for output to drain. This sleeps uninterruptibly because
|
|
|
|
* there is nothing intelligent we can do if interrupted. This
|
|
|
|
* means the process will be delayed in responding to the signal.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void pcm_write_sync(vwsnd_dev_t *devc)
|
|
|
|
{
|
|
|
|
vwsnd_port_t *wport = &devc->wport;
|
|
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
|
|
unsigned long flags;
|
|
|
|
vwsnd_port_hwstate_t hwstate;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p)\n", devc);
|
|
|
|
add_wait_queue(&wport->queue, &wait);
|
|
|
|
while (1) {
|
|
|
|
set_current_state(TASK_UNINTERRUPTIBLE);
|
|
|
|
spin_lock_irqsave(&wport->lock, flags);
|
|
|
|
{
|
|
|
|
hwstate = wport->hwstate;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&wport->lock, flags);
|
|
|
|
if (hwstate == HW_STOPPED)
|
|
|
|
break;
|
|
|
|
schedule();
|
|
|
|
}
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&wport->queue, &wait);
|
|
|
|
DBGPV("swstate = %d, hwstate = %d\n", wport->swstate, wport->hwstate);
|
|
|
|
DBGRV();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* audio driver */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* seek on an audio device always fails.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static void vwsnd_audio_read_intr(vwsnd_dev_t *devc, unsigned int status)
|
|
|
|
{
|
|
|
|
int overflown = status & LI_INTR_COMM1_OVERFLOW;
|
|
|
|
|
|
|
|
if (status & READ_INTR_MASK)
|
|
|
|
pcm_input(devc, overflown, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void vwsnd_audio_write_intr(vwsnd_dev_t *devc, unsigned int status)
|
|
|
|
{
|
|
|
|
int underflown = status & LI_INTR_COMM2_UNDERFLOW;
|
|
|
|
|
|
|
|
if (status & WRITE_INTR_MASK)
|
|
|
|
pcm_output(devc, underflown, 0);
|
|
|
|
}
|
|
|
|
|
IRQ: Maintain regs pointer globally rather than passing to IRQ handlers
Maintain a per-CPU global "struct pt_regs *" variable which can be used instead
of passing regs around manually through all ~1800 interrupt handlers in the
Linux kernel.
The regs pointer is used in few places, but it potentially costs both stack
space and code to pass it around. On the FRV arch, removing the regs parameter
from all the genirq function results in a 20% speed up of the IRQ exit path
(ie: from leaving timer_interrupt() to leaving do_IRQ()).
Where appropriate, an arch may override the generic storage facility and do
something different with the variable. On FRV, for instance, the address is
maintained in GR28 at all times inside the kernel as part of general exception
handling.
Having looked over the code, it appears that the parameter may be handed down
through up to twenty or so layers of functions. Consider a USB character
device attached to a USB hub, attached to a USB controller that posts its
interrupts through a cascaded auxiliary interrupt controller. A character
device driver may want to pass regs to the sysrq handler through the input
layer which adds another few layers of parameter passing.
I've build this code with allyesconfig for x86_64 and i386. I've runtested the
main part of the code on FRV and i386, though I can't test most of the drivers.
I've also done partial conversion for powerpc and MIPS - these at least compile
with minimal configurations.
This will affect all archs. Mostly the changes should be relatively easy.
Take do_IRQ(), store the regs pointer at the beginning, saving the old one:
struct pt_regs *old_regs = set_irq_regs(regs);
And put the old one back at the end:
set_irq_regs(old_regs);
Don't pass regs through to generic_handle_irq() or __do_IRQ().
In timer_interrupt(), this sort of change will be necessary:
- update_process_times(user_mode(regs));
- profile_tick(CPU_PROFILING, regs);
+ update_process_times(user_mode(get_irq_regs()));
+ profile_tick(CPU_PROFILING);
I'd like to move update_process_times()'s use of get_irq_regs() into itself,
except that i386, alone of the archs, uses something other than user_mode().
Some notes on the interrupt handling in the drivers:
(*) input_dev() is now gone entirely. The regs pointer is no longer stored in
the input_dev struct.
(*) finish_unlinks() in drivers/usb/host/ohci-q.c needs checking. It does
something different depending on whether it's been supplied with a regs
pointer or not.
(*) Various IRQ handler function pointers have been moved to type
irq_handler_t.
Signed-Off-By: David Howells <dhowells@redhat.com>
(cherry picked from 1b16e7ac850969f38b375e511e3fa2f474a33867 commit)
2006-10-05 21:55:46 +08:00
|
|
|
static irqreturn_t vwsnd_audio_intr(int irq, void *dev_id)
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
2006-10-07 03:00:58 +08:00
|
|
|
vwsnd_dev_t *devc = dev_id;
|
2005-04-17 06:20:36 +08:00
|
|
|
unsigned int status;
|
|
|
|
|
IRQ: Maintain regs pointer globally rather than passing to IRQ handlers
Maintain a per-CPU global "struct pt_regs *" variable which can be used instead
of passing regs around manually through all ~1800 interrupt handlers in the
Linux kernel.
The regs pointer is used in few places, but it potentially costs both stack
space and code to pass it around. On the FRV arch, removing the regs parameter
from all the genirq function results in a 20% speed up of the IRQ exit path
(ie: from leaving timer_interrupt() to leaving do_IRQ()).
Where appropriate, an arch may override the generic storage facility and do
something different with the variable. On FRV, for instance, the address is
maintained in GR28 at all times inside the kernel as part of general exception
handling.
Having looked over the code, it appears that the parameter may be handed down
through up to twenty or so layers of functions. Consider a USB character
device attached to a USB hub, attached to a USB controller that posts its
interrupts through a cascaded auxiliary interrupt controller. A character
device driver may want to pass regs to the sysrq handler through the input
layer which adds another few layers of parameter passing.
I've build this code with allyesconfig for x86_64 and i386. I've runtested the
main part of the code on FRV and i386, though I can't test most of the drivers.
I've also done partial conversion for powerpc and MIPS - these at least compile
with minimal configurations.
This will affect all archs. Mostly the changes should be relatively easy.
Take do_IRQ(), store the regs pointer at the beginning, saving the old one:
struct pt_regs *old_regs = set_irq_regs(regs);
And put the old one back at the end:
set_irq_regs(old_regs);
Don't pass regs through to generic_handle_irq() or __do_IRQ().
In timer_interrupt(), this sort of change will be necessary:
- update_process_times(user_mode(regs));
- profile_tick(CPU_PROFILING, regs);
+ update_process_times(user_mode(get_irq_regs()));
+ profile_tick(CPU_PROFILING);
I'd like to move update_process_times()'s use of get_irq_regs() into itself,
except that i386, alone of the archs, uses something other than user_mode().
Some notes on the interrupt handling in the drivers:
(*) input_dev() is now gone entirely. The regs pointer is no longer stored in
the input_dev struct.
(*) finish_unlinks() in drivers/usb/host/ohci-q.c needs checking. It does
something different depending on whether it's been supplied with a regs
pointer or not.
(*) Various IRQ handler function pointers have been moved to type
irq_handler_t.
Signed-Off-By: David Howells <dhowells@redhat.com>
(cherry picked from 1b16e7ac850969f38b375e511e3fa2f474a33867 commit)
2006-10-05 21:55:46 +08:00
|
|
|
DBGEV("(irq=%d, dev_id=0x%p)\n", irq, dev_id);
|
2005-04-17 06:20:36 +08:00
|
|
|
|
|
|
|
status = li_get_clear_intr_status(&devc->lith);
|
|
|
|
vwsnd_audio_read_intr(devc, status);
|
|
|
|
vwsnd_audio_write_intr(devc, status);
|
|
|
|
return IRQ_HANDLED;
|
|
|
|
}
|
|
|
|
|
|
|
|
static ssize_t vwsnd_audio_do_read(struct file *file,
|
|
|
|
char *buffer,
|
|
|
|
size_t count,
|
|
|
|
loff_t *ppos)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = file->private_data;
|
|
|
|
vwsnd_port_t *rport = ((file->f_mode & FMODE_READ) ?
|
|
|
|
&devc->rport : NULL);
|
|
|
|
int ret, nb;
|
|
|
|
|
|
|
|
DBGEV("(file=0x%p, buffer=0x%p, count=%d, ppos=0x%p)\n",
|
|
|
|
file, buffer, count, ppos);
|
|
|
|
|
|
|
|
if (!rport)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
if (rport->swbuf == NULL) {
|
|
|
|
vwsnd_port_t *wport = (file->f_mode & FMODE_WRITE) ?
|
|
|
|
&devc->wport : NULL;
|
|
|
|
ret = pcm_setup(devc, rport, wport);
|
|
|
|
if (ret < 0)
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!access_ok(VERIFY_READ, buffer, count))
|
|
|
|
return -EFAULT;
|
|
|
|
ret = 0;
|
|
|
|
while (count) {
|
|
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
|
|
add_wait_queue(&rport->queue, &wait);
|
|
|
|
while ((nb = swb_inc_u(rport, 0)) == 0) {
|
|
|
|
DBGPV("blocking\n");
|
|
|
|
set_current_state(TASK_INTERRUPTIBLE);
|
|
|
|
if (rport->flags & DISABLED ||
|
|
|
|
file->f_flags & O_NONBLOCK) {
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&rport->queue, &wait);
|
|
|
|
return ret ? ret : -EAGAIN;
|
|
|
|
}
|
|
|
|
schedule();
|
|
|
|
if (signal_pending(current)) {
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&rport->queue, &wait);
|
|
|
|
return ret ? ret : -ERESTARTSYS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&rport->queue, &wait);
|
|
|
|
pcm_input(devc, 0, 0);
|
|
|
|
/* nb bytes are available in userbuf. */
|
|
|
|
if (nb > count)
|
|
|
|
nb = count;
|
|
|
|
DBGPV("nb = %d\n", nb);
|
|
|
|
if (copy_to_user(buffer, rport->swbuf + rport->swb_u_idx, nb))
|
|
|
|
return -EFAULT;
|
|
|
|
(void) swb_inc_u(rport, nb);
|
|
|
|
buffer += nb;
|
|
|
|
count -= nb;
|
|
|
|
ret += nb;
|
|
|
|
}
|
|
|
|
DBGPV("returning %d\n", ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static ssize_t vwsnd_audio_read(struct file *file,
|
|
|
|
char *buffer,
|
|
|
|
size_t count,
|
|
|
|
loff_t *ppos)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = file->private_data;
|
|
|
|
ssize_t ret;
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
ret = vwsnd_audio_do_read(file, buffer, count, ppos);
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static ssize_t vwsnd_audio_do_write(struct file *file,
|
|
|
|
const char *buffer,
|
|
|
|
size_t count,
|
|
|
|
loff_t *ppos)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = file->private_data;
|
|
|
|
vwsnd_port_t *wport = ((file->f_mode & FMODE_WRITE) ?
|
|
|
|
&devc->wport : NULL);
|
|
|
|
int ret, nb;
|
|
|
|
|
|
|
|
DBGEV("(file=0x%p, buffer=0x%p, count=%d, ppos=0x%p)\n",
|
|
|
|
file, buffer, count, ppos);
|
|
|
|
|
|
|
|
if (!wport)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
if (wport->swbuf == NULL) {
|
|
|
|
vwsnd_port_t *rport = (file->f_mode & FMODE_READ) ?
|
|
|
|
&devc->rport : NULL;
|
|
|
|
ret = pcm_setup(devc, rport, wport);
|
|
|
|
if (ret < 0)
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
if (!access_ok(VERIFY_WRITE, buffer, count))
|
|
|
|
return -EFAULT;
|
|
|
|
ret = 0;
|
|
|
|
while (count) {
|
|
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
|
|
add_wait_queue(&wport->queue, &wait);
|
|
|
|
while ((nb = swb_inc_u(wport, 0)) == 0) {
|
|
|
|
set_current_state(TASK_INTERRUPTIBLE);
|
|
|
|
if (wport->flags & DISABLED ||
|
|
|
|
file->f_flags & O_NONBLOCK) {
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&wport->queue, &wait);
|
|
|
|
return ret ? ret : -EAGAIN;
|
|
|
|
}
|
|
|
|
schedule();
|
|
|
|
if (signal_pending(current)) {
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&wport->queue, &wait);
|
|
|
|
return ret ? ret : -ERESTARTSYS;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
current->state = TASK_RUNNING;
|
|
|
|
remove_wait_queue(&wport->queue, &wait);
|
|
|
|
/* nb bytes are available in userbuf. */
|
|
|
|
if (nb > count)
|
|
|
|
nb = count;
|
|
|
|
DBGPV("nb = %d\n", nb);
|
|
|
|
if (copy_from_user(wport->swbuf + wport->swb_u_idx, buffer, nb))
|
|
|
|
return -EFAULT;
|
|
|
|
pcm_output(devc, 0, nb);
|
|
|
|
buffer += nb;
|
|
|
|
count -= nb;
|
|
|
|
ret += nb;
|
|
|
|
}
|
|
|
|
DBGPV("returning %d\n", ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static ssize_t vwsnd_audio_write(struct file *file,
|
|
|
|
const char *buffer,
|
|
|
|
size_t count,
|
|
|
|
loff_t *ppos)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = file->private_data;
|
|
|
|
ssize_t ret;
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
ret = vwsnd_audio_do_write(file, buffer, count, ppos);
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* No kernel lock - fine */
|
|
|
|
static unsigned int vwsnd_audio_poll(struct file *file,
|
|
|
|
struct poll_table_struct *wait)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = (vwsnd_dev_t *) file->private_data;
|
|
|
|
vwsnd_port_t *rport = (file->f_mode & FMODE_READ) ?
|
|
|
|
&devc->rport : NULL;
|
|
|
|
vwsnd_port_t *wport = (file->f_mode & FMODE_WRITE) ?
|
|
|
|
&devc->wport : NULL;
|
|
|
|
unsigned int mask = 0;
|
|
|
|
|
|
|
|
DBGEV("(file=0x%p, wait=0x%p)\n", file, wait);
|
|
|
|
|
|
|
|
ASSERT(rport || wport);
|
|
|
|
if (rport) {
|
|
|
|
poll_wait(file, &rport->queue, wait);
|
|
|
|
if (swb_inc_u(rport, 0))
|
|
|
|
mask |= (POLLIN | POLLRDNORM);
|
|
|
|
}
|
|
|
|
if (wport) {
|
|
|
|
poll_wait(file, &wport->queue, wait);
|
|
|
|
if (wport->swbuf == NULL || swb_inc_u(wport, 0))
|
|
|
|
mask |= (POLLOUT | POLLWRNORM);
|
|
|
|
}
|
|
|
|
|
|
|
|
DBGPV("returning 0x%x\n", mask);
|
|
|
|
return mask;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int vwsnd_audio_do_ioctl(struct inode *inode,
|
|
|
|
struct file *file,
|
|
|
|
unsigned int cmd,
|
|
|
|
unsigned long arg)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = (vwsnd_dev_t *) file->private_data;
|
|
|
|
vwsnd_port_t *rport = (file->f_mode & FMODE_READ) ?
|
|
|
|
&devc->rport : NULL;
|
|
|
|
vwsnd_port_t *wport = (file->f_mode & FMODE_WRITE) ?
|
|
|
|
&devc->wport : NULL;
|
|
|
|
vwsnd_port_t *aport = rport ? rport : wport;
|
|
|
|
struct audio_buf_info buf_info;
|
|
|
|
struct count_info info;
|
|
|
|
unsigned long flags;
|
|
|
|
int ival;
|
|
|
|
|
|
|
|
|
|
|
|
DBGEV("(inode=0x%p, file=0x%p, cmd=0x%x, arg=0x%lx)\n",
|
|
|
|
inode, file, cmd, arg);
|
|
|
|
switch (cmd) {
|
|
|
|
case OSS_GETVERSION: /* _SIOR ('M', 118, int) */
|
|
|
|
DBGX("OSS_GETVERSION\n");
|
|
|
|
ival = SOUND_VERSION;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETCAPS: /* _SIOR ('P',15, int) */
|
|
|
|
DBGX("SNDCTL_DSP_GETCAPS\n");
|
|
|
|
ival = DSP_CAP_DUPLEX | DSP_CAP_REALTIME | DSP_CAP_TRIGGER;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETFMTS: /* _SIOR ('P',11, int) */
|
|
|
|
DBGX("SNDCTL_DSP_GETFMTS\n");
|
|
|
|
ival = (AFMT_S16_LE | AFMT_MU_LAW | AFMT_A_LAW |
|
|
|
|
AFMT_U8 | AFMT_S8);
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_PCM_READ_RATE: /* _SIOR ('P', 2, int) */
|
|
|
|
DBGX("SOUND_PCM_READ_RATE\n");
|
|
|
|
ival = aport->sw_framerate;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SOUND_PCM_READ_CHANNELS: /* _SIOR ('P', 6, int) */
|
|
|
|
DBGX("SOUND_PCM_READ_CHANNELS\n");
|
|
|
|
ival = aport->sw_channels;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_SPEED: /* _SIOWR('P', 2, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_SPEED %d\n", ival);
|
|
|
|
if (ival) {
|
|
|
|
if (aport->swstate != SW_INITIAL) {
|
|
|
|
DBGX("SNDCTL_DSP_SPEED failed: swstate = %d\n",
|
|
|
|
aport->swstate);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
if (ival < MIN_SPEED)
|
|
|
|
ival = MIN_SPEED;
|
|
|
|
if (ival > MAX_SPEED)
|
|
|
|
ival = MAX_SPEED;
|
|
|
|
if (rport)
|
|
|
|
rport->sw_framerate = ival;
|
|
|
|
if (wport)
|
|
|
|
wport->sw_framerate = ival;
|
|
|
|
} else
|
|
|
|
ival = aport->sw_framerate;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_STEREO: /* _SIOWR('P', 3, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_STEREO %d\n", ival);
|
|
|
|
if (ival != 0 && ival != 1)
|
|
|
|
return -EINVAL;
|
|
|
|
if (aport->swstate != SW_INITIAL)
|
|
|
|
return -EINVAL;
|
|
|
|
if (rport)
|
|
|
|
rport->sw_channels = ival + 1;
|
|
|
|
if (wport)
|
|
|
|
wport->sw_channels = ival + 1;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_CHANNELS: /* _SIOWR('P', 6, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_CHANNELS %d\n", ival);
|
|
|
|
if (ival != 1 && ival != 2)
|
|
|
|
return -EINVAL;
|
|
|
|
if (aport->swstate != SW_INITIAL)
|
|
|
|
return -EINVAL;
|
|
|
|
if (rport)
|
|
|
|
rport->sw_channels = ival;
|
|
|
|
if (wport)
|
|
|
|
wport->sw_channels = ival;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETBLKSIZE: /* _SIOWR('P', 4, int) */
|
|
|
|
ival = pcm_setup(devc, rport, wport);
|
|
|
|
if (ival < 0) {
|
|
|
|
DBGX("SNDCTL_DSP_GETBLKSIZE failed, errno %d\n", ival);
|
|
|
|
return ival;
|
|
|
|
}
|
|
|
|
ival = 1 << aport->sw_fragshift;
|
|
|
|
DBGX("SNDCTL_DSP_GETBLKSIZE returning %d\n", ival);
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_SETFRAGMENT: /* _SIOWR('P',10, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_SETFRAGMENT %d:%d\n",
|
|
|
|
ival >> 16, ival & 0xFFFF);
|
|
|
|
if (aport->swstate != SW_INITIAL)
|
|
|
|
return -EINVAL;
|
|
|
|
{
|
|
|
|
int sw_fragshift = ival & 0xFFFF;
|
|
|
|
int sw_subdivshift = aport->sw_subdivshift;
|
|
|
|
int hw_fragshift = sw_fragshift - sw_subdivshift;
|
|
|
|
int sw_fragcount = (ival >> 16) & 0xFFFF;
|
|
|
|
int hw_fragsize;
|
|
|
|
if (hw_fragshift < MIN_FRAGSHIFT)
|
|
|
|
hw_fragshift = MIN_FRAGSHIFT;
|
|
|
|
if (hw_fragshift > MAX_FRAGSHIFT)
|
|
|
|
hw_fragshift = MAX_FRAGSHIFT;
|
|
|
|
sw_fragshift = hw_fragshift + aport->sw_subdivshift;
|
|
|
|
hw_fragsize = 1 << hw_fragshift;
|
|
|
|
if (sw_fragcount < MIN_FRAGCOUNT(hw_fragsize))
|
|
|
|
sw_fragcount = MIN_FRAGCOUNT(hw_fragsize);
|
|
|
|
if (sw_fragcount > MAX_FRAGCOUNT(hw_fragsize))
|
|
|
|
sw_fragcount = MAX_FRAGCOUNT(hw_fragsize);
|
|
|
|
DBGPV("sw_fragshift = %d\n", sw_fragshift);
|
|
|
|
DBGPV("rport = 0x%p, wport = 0x%p\n", rport, wport);
|
|
|
|
if (rport) {
|
|
|
|
rport->sw_fragshift = sw_fragshift;
|
|
|
|
rport->sw_fragcount = sw_fragcount;
|
|
|
|
}
|
|
|
|
if (wport) {
|
|
|
|
wport->sw_fragshift = sw_fragshift;
|
|
|
|
wport->sw_fragcount = sw_fragcount;
|
|
|
|
}
|
|
|
|
ival = sw_fragcount << 16 | sw_fragshift;
|
|
|
|
}
|
|
|
|
DBGX("SNDCTL_DSP_SETFRAGMENT returns %d:%d\n",
|
|
|
|
ival >> 16, ival & 0xFFFF);
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_SUBDIVIDE: /* _SIOWR('P', 9, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_SUBDIVIDE %d\n", ival);
|
|
|
|
if (aport->swstate != SW_INITIAL)
|
|
|
|
return -EINVAL;
|
|
|
|
{
|
|
|
|
int subdivshift;
|
|
|
|
int hw_fragshift, hw_fragsize, hw_fragcount;
|
|
|
|
switch (ival) {
|
|
|
|
case 1: subdivshift = 0; break;
|
|
|
|
case 2: subdivshift = 1; break;
|
|
|
|
case 4: subdivshift = 2; break;
|
|
|
|
default: return -EINVAL;
|
|
|
|
}
|
|
|
|
hw_fragshift = aport->sw_fragshift - subdivshift;
|
|
|
|
if (hw_fragshift < MIN_FRAGSHIFT ||
|
|
|
|
hw_fragshift > MAX_FRAGSHIFT)
|
|
|
|
return -EINVAL;
|
|
|
|
hw_fragsize = 1 << hw_fragshift;
|
|
|
|
hw_fragcount = aport->sw_fragcount >> subdivshift;
|
|
|
|
if (hw_fragcount < MIN_FRAGCOUNT(hw_fragsize) ||
|
|
|
|
hw_fragcount > MAX_FRAGCOUNT(hw_fragsize))
|
|
|
|
return -EINVAL;
|
|
|
|
if (rport)
|
|
|
|
rport->sw_subdivshift = subdivshift;
|
|
|
|
if (wport)
|
|
|
|
wport->sw_subdivshift = subdivshift;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_SETFMT: /* _SIOWR('P',5, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_SETFMT %d\n", ival);
|
|
|
|
if (ival != AFMT_QUERY) {
|
|
|
|
if (aport->swstate != SW_INITIAL) {
|
|
|
|
DBGP("SETFMT failed, swstate = %d\n",
|
|
|
|
aport->swstate);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
switch (ival) {
|
|
|
|
case AFMT_MU_LAW:
|
|
|
|
case AFMT_A_LAW:
|
|
|
|
case AFMT_U8:
|
|
|
|
case AFMT_S8:
|
|
|
|
case AFMT_S16_LE:
|
|
|
|
if (rport)
|
|
|
|
rport->sw_samplefmt = ival;
|
|
|
|
if (wport)
|
|
|
|
wport->sw_samplefmt = ival;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ival = aport->sw_samplefmt;
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETOSPACE: /* _SIOR ('P',12, audio_buf_info) */
|
|
|
|
DBGXV("SNDCTL_DSP_GETOSPACE\n");
|
|
|
|
if (!wport)
|
|
|
|
return -EINVAL;
|
|
|
|
ival = pcm_setup(devc, rport, wport);
|
|
|
|
if (ival < 0)
|
|
|
|
return ival;
|
|
|
|
ival = swb_inc_u(wport, 0);
|
|
|
|
buf_info.fragments = ival >> wport->sw_fragshift;
|
|
|
|
buf_info.fragstotal = wport->sw_fragcount;
|
|
|
|
buf_info.fragsize = 1 << wport->sw_fragshift;
|
|
|
|
buf_info.bytes = ival;
|
|
|
|
DBGXV("SNDCTL_DSP_GETOSPACE returns { %d %d %d %d }\n",
|
|
|
|
buf_info.fragments, buf_info.fragstotal,
|
|
|
|
buf_info.fragsize, buf_info.bytes);
|
|
|
|
if (copy_to_user((void *) arg, &buf_info, sizeof buf_info))
|
|
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETISPACE: /* _SIOR ('P',13, audio_buf_info) */
|
|
|
|
DBGX("SNDCTL_DSP_GETISPACE\n");
|
|
|
|
if (!rport)
|
|
|
|
return -EINVAL;
|
|
|
|
ival = pcm_setup(devc, rport, wport);
|
|
|
|
if (ival < 0)
|
|
|
|
return ival;
|
|
|
|
ival = swb_inc_u(rport, 0);
|
|
|
|
buf_info.fragments = ival >> rport->sw_fragshift;
|
|
|
|
buf_info.fragstotal = rport->sw_fragcount;
|
|
|
|
buf_info.fragsize = 1 << rport->sw_fragshift;
|
|
|
|
buf_info.bytes = ival;
|
|
|
|
DBGX("SNDCTL_DSP_GETISPACE returns { %d %d %d %d }\n",
|
|
|
|
buf_info.fragments, buf_info.fragstotal,
|
|
|
|
buf_info.fragsize, buf_info.bytes);
|
|
|
|
if (copy_to_user((void *) arg, &buf_info, sizeof buf_info))
|
|
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_NONBLOCK: /* _SIO ('P',14) */
|
|
|
|
DBGX("SNDCTL_DSP_NONBLOCK\n");
|
2009-02-07 06:25:24 +08:00
|
|
|
spin_lock(&file->f_lock);
|
2005-04-17 06:20:36 +08:00
|
|
|
file->f_flags |= O_NONBLOCK;
|
2009-02-07 06:25:24 +08:00
|
|
|
spin_unlock(&file->f_lock);
|
2005-04-17 06:20:36 +08:00
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_RESET: /* _SIO ('P', 0) */
|
|
|
|
DBGX("SNDCTL_DSP_RESET\n");
|
|
|
|
/*
|
|
|
|
* Nothing special needs to be done for input. Input
|
|
|
|
* samples sit in swbuf, but it will be reinitialized
|
|
|
|
* to empty when pcm_setup() is called.
|
|
|
|
*/
|
|
|
|
if (wport && wport->swbuf) {
|
|
|
|
wport->swstate = SW_INITIAL;
|
|
|
|
pcm_output(devc, 0, 0);
|
|
|
|
pcm_write_sync(devc);
|
|
|
|
}
|
|
|
|
pcm_shutdown(devc, rport, wport);
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_SYNC: /* _SIO ('P', 1) */
|
|
|
|
DBGX("SNDCTL_DSP_SYNC\n");
|
|
|
|
if (wport) {
|
|
|
|
pcm_flush_frag(devc);
|
|
|
|
pcm_write_sync(devc);
|
|
|
|
}
|
|
|
|
pcm_shutdown(devc, rport, wport);
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_POST: /* _SIO ('P', 8) */
|
|
|
|
DBGX("SNDCTL_DSP_POST\n");
|
|
|
|
if (!wport)
|
|
|
|
return -EINVAL;
|
|
|
|
pcm_flush_frag(devc);
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETIPTR: /* _SIOR ('P', 17, count_info) */
|
|
|
|
DBGX("SNDCTL_DSP_GETIPTR\n");
|
|
|
|
if (!rport)
|
|
|
|
return -EINVAL;
|
|
|
|
spin_lock_irqsave(&rport->lock, flags);
|
|
|
|
{
|
|
|
|
ustmsc_t ustmsc;
|
|
|
|
if (rport->hwstate == HW_RUNNING) {
|
|
|
|
ASSERT(rport->swstate == SW_RUN);
|
|
|
|
li_read_USTMSC(&rport->chan, &ustmsc);
|
|
|
|
info.bytes = ustmsc.msc - rport->MSC_offset;
|
|
|
|
info.bytes *= rport->frame_size;
|
|
|
|
} else {
|
|
|
|
info.bytes = rport->byte_count;
|
|
|
|
}
|
|
|
|
info.blocks = rport->frag_count;
|
|
|
|
info.ptr = 0; /* not implemented */
|
|
|
|
rport->frag_count = 0;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&rport->lock, flags);
|
|
|
|
if (copy_to_user((void *) arg, &info, sizeof info))
|
|
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETOPTR: /* _SIOR ('P',18, count_info) */
|
|
|
|
DBGX("SNDCTL_DSP_GETOPTR\n");
|
|
|
|
if (!wport)
|
|
|
|
return -EINVAL;
|
|
|
|
spin_lock_irqsave(&wport->lock, flags);
|
|
|
|
{
|
|
|
|
ustmsc_t ustmsc;
|
|
|
|
if (wport->hwstate == HW_RUNNING) {
|
|
|
|
ASSERT(wport->swstate == SW_RUN);
|
|
|
|
li_read_USTMSC(&wport->chan, &ustmsc);
|
|
|
|
info.bytes = ustmsc.msc - wport->MSC_offset;
|
|
|
|
info.bytes *= wport->frame_size;
|
|
|
|
} else {
|
|
|
|
info.bytes = wport->byte_count;
|
|
|
|
}
|
|
|
|
info.blocks = wport->frag_count;
|
|
|
|
info.ptr = 0; /* not implemented */
|
|
|
|
wport->frag_count = 0;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&wport->lock, flags);
|
|
|
|
if (copy_to_user((void *) arg, &info, sizeof info))
|
|
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETODELAY: /* _SIOR ('P', 23, int) */
|
|
|
|
DBGX("SNDCTL_DSP_GETODELAY\n");
|
|
|
|
if (!wport)
|
|
|
|
return -EINVAL;
|
|
|
|
spin_lock_irqsave(&wport->lock, flags);
|
|
|
|
{
|
|
|
|
int fsize = wport->frame_size;
|
|
|
|
ival = wport->swb_i_avail / fsize;
|
|
|
|
if (wport->hwstate == HW_RUNNING) {
|
|
|
|
int swptr, hwptr, hwframes, hwbytes, hwsize;
|
|
|
|
int totalhwbytes;
|
|
|
|
ustmsc_t ustmsc;
|
|
|
|
|
|
|
|
hwsize = wport->hwbuf_size;
|
|
|
|
swptr = li_read_swptr(&wport->chan);
|
|
|
|
li_read_USTMSC(&wport->chan, &ustmsc);
|
|
|
|
hwframes = ustmsc.msc - wport->MSC_offset;
|
|
|
|
totalhwbytes = hwframes * fsize;
|
|
|
|
hwptr = totalhwbytes % hwsize;
|
|
|
|
hwbytes = (swptr - hwptr + hwsize) % hwsize;
|
|
|
|
ival += hwbytes / fsize;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&wport->lock, flags);
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_PROFILE: /* _SIOW ('P', 23, int) */
|
|
|
|
DBGX("SNDCTL_DSP_PROFILE\n");
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Thomas Sailer explains SNDCTL_DSP_PROFILE
|
|
|
|
* (private email, March 24, 1999):
|
|
|
|
*
|
|
|
|
* This gives the sound driver a hint on what it
|
|
|
|
* should do with partial fragments
|
|
|
|
* (i.e. fragments partially filled with write).
|
|
|
|
* This can direct the driver to zero them or
|
|
|
|
* leave them alone. But don't ask me what this
|
|
|
|
* is good for, my driver just zeroes the last
|
|
|
|
* fragment before the receiver stops, no idea
|
|
|
|
* what good for any other behaviour could
|
|
|
|
* be. Implementing it as NOP seems safe.
|
|
|
|
*/
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SNDCTL_DSP_GETTRIGGER: /* _SIOR ('P',16, int) */
|
|
|
|
DBGX("SNDCTL_DSP_GETTRIGGER\n");
|
|
|
|
ival = 0;
|
|
|
|
if (rport) {
|
|
|
|
spin_lock_irqsave(&rport->lock, flags);
|
|
|
|
{
|
|
|
|
if (!(rport->flags & DISABLED))
|
|
|
|
ival |= PCM_ENABLE_INPUT;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&rport->lock, flags);
|
|
|
|
}
|
|
|
|
if (wport) {
|
|
|
|
spin_lock_irqsave(&wport->lock, flags);
|
|
|
|
{
|
|
|
|
if (!(wport->flags & DISABLED))
|
|
|
|
ival |= PCM_ENABLE_OUTPUT;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&wport->lock, flags);
|
|
|
|
}
|
|
|
|
return put_user(ival, (int *) arg);
|
|
|
|
|
|
|
|
case SNDCTL_DSP_SETTRIGGER: /* _SIOW ('P',16, int) */
|
|
|
|
if (get_user(ival, (int *) arg))
|
|
|
|
return -EFAULT;
|
|
|
|
DBGX("SNDCTL_DSP_SETTRIGGER %d\n", ival);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If user is disabling I/O and port is not in initial
|
|
|
|
* state, fail with EINVAL.
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (((rport && !(ival & PCM_ENABLE_INPUT)) ||
|
|
|
|
(wport && !(ival & PCM_ENABLE_OUTPUT))) &&
|
|
|
|
aport->swstate != SW_INITIAL)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
if (rport) {
|
|
|
|
vwsnd_port_hwstate_t hwstate;
|
|
|
|
spin_lock_irqsave(&rport->lock, flags);
|
|
|
|
{
|
|
|
|
hwstate = rport->hwstate;
|
|
|
|
if (ival & PCM_ENABLE_INPUT)
|
|
|
|
rport->flags &= ~DISABLED;
|
|
|
|
else
|
|
|
|
rport->flags |= DISABLED;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&rport->lock, flags);
|
|
|
|
if (hwstate != HW_RUNNING && ival & PCM_ENABLE_INPUT) {
|
|
|
|
|
|
|
|
if (rport->swstate == SW_INITIAL)
|
|
|
|
pcm_setup(devc, rport, wport);
|
|
|
|
else
|
|
|
|
li_activate_dma(&rport->chan);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (wport) {
|
|
|
|
vwsnd_port_flags_t pflags;
|
|
|
|
spin_lock_irqsave(&wport->lock, flags);
|
|
|
|
{
|
|
|
|
pflags = wport->flags;
|
|
|
|
if (ival & PCM_ENABLE_OUTPUT)
|
|
|
|
wport->flags &= ~DISABLED;
|
|
|
|
else
|
|
|
|
wport->flags |= DISABLED;
|
|
|
|
}
|
|
|
|
spin_unlock_irqrestore(&wport->lock, flags);
|
|
|
|
if (pflags & DISABLED && ival & PCM_ENABLE_OUTPUT) {
|
|
|
|
if (wport->swstate == SW_RUN)
|
|
|
|
pcm_output(devc, 0, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
default:
|
|
|
|
DBGP("unknown ioctl 0x%x\n", cmd);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
DBGP("unimplemented ioctl 0x%x\n", cmd);
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int vwsnd_audio_ioctl(struct inode *inode,
|
|
|
|
struct file *file,
|
|
|
|
unsigned int cmd,
|
|
|
|
unsigned long arg)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = (vwsnd_dev_t *) file->private_data;
|
|
|
|
int ret;
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
ret = vwsnd_audio_do_ioctl(inode, file, cmd, arg);
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* No mmap. */
|
|
|
|
|
|
|
|
static int vwsnd_audio_mmap(struct file *file, struct vm_area_struct *vma)
|
|
|
|
{
|
|
|
|
DBGE("(file=0x%p, vma=0x%p)\n", file, vma);
|
|
|
|
return -ENODEV;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Open the audio device for read and/or write.
|
|
|
|
*
|
|
|
|
* Returns 0 on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int vwsnd_audio_open(struct inode *inode, struct file *file)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc;
|
|
|
|
int minor = iminor(inode);
|
|
|
|
int sw_samplefmt;
|
|
|
|
|
|
|
|
DBGE("(inode=0x%p, file=0x%p)\n", inode, file);
|
|
|
|
|
2010-07-11 18:16:36 +08:00
|
|
|
lock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
INC_USE_COUNT;
|
|
|
|
for (devc = vwsnd_dev_list; devc; devc = devc->next_dev)
|
|
|
|
if ((devc->audio_minor & ~0x0F) == (minor & ~0x0F))
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (devc == NULL) {
|
|
|
|
DEC_USE_COUNT;
|
2010-07-11 18:16:36 +08:00
|
|
|
unlock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
return -ENODEV;
|
|
|
|
}
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->open_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
while (devc->open_mode & file->f_mode) {
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->open_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
|
|
DEC_USE_COUNT;
|
2010-07-11 18:16:36 +08:00
|
|
|
unlock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
return -EBUSY;
|
|
|
|
}
|
|
|
|
interruptible_sleep_on(&devc->open_wait);
|
|
|
|
if (signal_pending(current)) {
|
|
|
|
DEC_USE_COUNT;
|
2010-07-11 18:16:36 +08:00
|
|
|
unlock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
return -ERESTARTSYS;
|
|
|
|
}
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->open_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
}
|
|
|
|
devc->open_mode |= file->f_mode & (FMODE_READ | FMODE_WRITE);
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->open_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
|
|
|
|
/* get default sample format from minor number. */
|
|
|
|
|
|
|
|
sw_samplefmt = 0;
|
|
|
|
if ((minor & 0xF) == SND_DEV_DSP)
|
|
|
|
sw_samplefmt = AFMT_U8;
|
|
|
|
else if ((minor & 0xF) == SND_DEV_AUDIO)
|
|
|
|
sw_samplefmt = AFMT_MU_LAW;
|
|
|
|
else if ((minor & 0xF) == SND_DEV_DSP16)
|
|
|
|
sw_samplefmt = AFMT_S16_LE;
|
|
|
|
else
|
|
|
|
ASSERT(0);
|
|
|
|
|
|
|
|
/* Initialize vwsnd_ports. */
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
|
|
|
if (file->f_mode & FMODE_READ) {
|
|
|
|
devc->rport.swstate = SW_INITIAL;
|
|
|
|
devc->rport.flags = 0;
|
|
|
|
devc->rport.sw_channels = 1;
|
|
|
|
devc->rport.sw_samplefmt = sw_samplefmt;
|
|
|
|
devc->rport.sw_framerate = 8000;
|
|
|
|
devc->rport.sw_fragshift = DEFAULT_FRAGSHIFT;
|
|
|
|
devc->rport.sw_fragcount = DEFAULT_FRAGCOUNT;
|
|
|
|
devc->rport.sw_subdivshift = DEFAULT_SUBDIVSHIFT;
|
|
|
|
devc->rport.byte_count = 0;
|
|
|
|
devc->rport.frag_count = 0;
|
|
|
|
}
|
|
|
|
if (file->f_mode & FMODE_WRITE) {
|
|
|
|
devc->wport.swstate = SW_INITIAL;
|
|
|
|
devc->wport.flags = 0;
|
|
|
|
devc->wport.sw_channels = 1;
|
|
|
|
devc->wport.sw_samplefmt = sw_samplefmt;
|
|
|
|
devc->wport.sw_framerate = 8000;
|
|
|
|
devc->wport.sw_fragshift = DEFAULT_FRAGSHIFT;
|
|
|
|
devc->wport.sw_fragcount = DEFAULT_FRAGCOUNT;
|
|
|
|
devc->wport.sw_subdivshift = DEFAULT_SUBDIVSHIFT;
|
|
|
|
devc->wport.byte_count = 0;
|
|
|
|
devc->wport.frag_count = 0;
|
|
|
|
}
|
|
|
|
}
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
|
|
|
|
file->private_data = devc;
|
|
|
|
DBGRV();
|
2010-07-11 18:16:36 +08:00
|
|
|
unlock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Release (close) the audio device.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int vwsnd_audio_release(struct inode *inode, struct file *file)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = (vwsnd_dev_t *) file->private_data;
|
|
|
|
vwsnd_port_t *wport = NULL, *rport = NULL;
|
|
|
|
int err = 0;
|
|
|
|
|
|
|
|
lock_kernel();
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
|
|
|
DBGEV("(inode=0x%p, file=0x%p)\n", inode, file);
|
|
|
|
|
|
|
|
if (file->f_mode & FMODE_READ)
|
|
|
|
rport = &devc->rport;
|
|
|
|
if (file->f_mode & FMODE_WRITE) {
|
|
|
|
wport = &devc->wport;
|
|
|
|
pcm_flush_frag(devc);
|
|
|
|
pcm_write_sync(devc);
|
|
|
|
}
|
|
|
|
pcm_shutdown(devc, rport, wport);
|
|
|
|
if (rport)
|
|
|
|
rport->swstate = SW_OFF;
|
|
|
|
if (wport)
|
|
|
|
wport->swstate = SW_OFF;
|
|
|
|
}
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->io_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->open_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
|
|
|
devc->open_mode &= ~file->f_mode;
|
|
|
|
}
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->open_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
wake_up(&devc->open_wait);
|
|
|
|
DEC_USE_COUNT;
|
|
|
|
DBGR();
|
|
|
|
unlock_kernel();
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
|
2007-02-12 16:55:37 +08:00
|
|
|
static const struct file_operations vwsnd_audio_fops = {
|
2005-04-17 06:20:36 +08:00
|
|
|
.owner = THIS_MODULE,
|
|
|
|
.llseek = no_llseek,
|
|
|
|
.read = vwsnd_audio_read,
|
|
|
|
.write = vwsnd_audio_write,
|
|
|
|
.poll = vwsnd_audio_poll,
|
|
|
|
.ioctl = vwsnd_audio_ioctl,
|
|
|
|
.mmap = vwsnd_audio_mmap,
|
|
|
|
.open = vwsnd_audio_open,
|
|
|
|
.release = vwsnd_audio_release,
|
|
|
|
};
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* mixer driver */
|
|
|
|
|
|
|
|
/* open the mixer device. */
|
|
|
|
|
|
|
|
static int vwsnd_mixer_open(struct inode *inode, struct file *file)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc;
|
|
|
|
|
|
|
|
DBGEV("(inode=0x%p, file=0x%p)\n", inode, file);
|
|
|
|
|
|
|
|
INC_USE_COUNT;
|
2010-07-11 18:16:36 +08:00
|
|
|
lock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
for (devc = vwsnd_dev_list; devc; devc = devc->next_dev)
|
|
|
|
if (devc->mixer_minor == iminor(inode))
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (devc == NULL) {
|
|
|
|
DEC_USE_COUNT;
|
2010-07-11 18:16:36 +08:00
|
|
|
unlock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
return -ENODEV;
|
|
|
|
}
|
|
|
|
file->private_data = devc;
|
2010-07-11 18:16:36 +08:00
|
|
|
unlock_kernel();
|
2005-04-17 06:20:36 +08:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* release (close) the mixer device. */
|
|
|
|
|
|
|
|
static int vwsnd_mixer_release(struct inode *inode, struct file *file)
|
|
|
|
{
|
|
|
|
DBGEV("(inode=0x%p, file=0x%p)\n", inode, file);
|
|
|
|
DEC_USE_COUNT;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* mixer_read_ioctl handles all read ioctls on the mixer device. */
|
|
|
|
|
|
|
|
static int mixer_read_ioctl(vwsnd_dev_t *devc, unsigned int nr, void __user *arg)
|
|
|
|
{
|
|
|
|
int val = -1;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p, nr=0x%x, arg=0x%p)\n", devc, nr, arg);
|
|
|
|
|
|
|
|
switch (nr) {
|
|
|
|
case SOUND_MIXER_CAPS:
|
|
|
|
val = SOUND_CAP_EXCL_INPUT;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_DEVMASK:
|
|
|
|
val = (SOUND_MASK_PCM | SOUND_MASK_LINE |
|
|
|
|
SOUND_MASK_MIC | SOUND_MASK_CD | SOUND_MASK_RECLEV);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_STEREODEVS:
|
|
|
|
val = (SOUND_MASK_PCM | SOUND_MASK_LINE |
|
|
|
|
SOUND_MASK_MIC | SOUND_MASK_CD | SOUND_MASK_RECLEV);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_OUTMASK:
|
|
|
|
val = (SOUND_MASK_PCM | SOUND_MASK_LINE |
|
|
|
|
SOUND_MASK_MIC | SOUND_MASK_CD);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_RECMASK:
|
|
|
|
val = (SOUND_MASK_PCM | SOUND_MASK_LINE |
|
|
|
|
SOUND_MASK_MIC | SOUND_MASK_CD);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_PCM:
|
|
|
|
val = ad1843_get_gain(&devc->lith, &ad1843_gain_PCM);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_LINE:
|
|
|
|
val = ad1843_get_gain(&devc->lith, &ad1843_gain_LINE);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_MIC:
|
|
|
|
val = ad1843_get_gain(&devc->lith, &ad1843_gain_MIC);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_CD:
|
|
|
|
val = ad1843_get_gain(&devc->lith, &ad1843_gain_CD);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_RECLEV:
|
|
|
|
val = ad1843_get_gain(&devc->lith, &ad1843_gain_RECLEV);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_RECSRC:
|
|
|
|
val = ad1843_get_recsrc(&devc->lith);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_OUTSRC:
|
|
|
|
val = ad1843_get_outsrc(&devc->lith);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
return put_user(val, (int __user *) arg);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* mixer_write_ioctl handles all write ioctls on the mixer device. */
|
|
|
|
|
|
|
|
static int mixer_write_ioctl(vwsnd_dev_t *devc, unsigned int nr, void __user *arg)
|
|
|
|
{
|
|
|
|
int val;
|
|
|
|
int err;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p, nr=0x%x, arg=0x%p)\n", devc, nr, arg);
|
|
|
|
|
|
|
|
err = get_user(val, (int __user *) arg);
|
|
|
|
if (err)
|
|
|
|
return -EFAULT;
|
|
|
|
switch (nr) {
|
|
|
|
case SOUND_MIXER_PCM:
|
|
|
|
val = ad1843_set_gain(&devc->lith, &ad1843_gain_PCM, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_LINE:
|
|
|
|
val = ad1843_set_gain(&devc->lith, &ad1843_gain_LINE, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_MIC:
|
|
|
|
val = ad1843_set_gain(&devc->lith, &ad1843_gain_MIC, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_CD:
|
|
|
|
val = ad1843_set_gain(&devc->lith, &ad1843_gain_CD, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_RECLEV:
|
|
|
|
val = ad1843_set_gain(&devc->lith, &ad1843_gain_RECLEV, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_RECSRC:
|
|
|
|
if (devc->rport.swbuf || devc->wport.swbuf)
|
|
|
|
return -EBUSY; /* can't change recsrc while running */
|
|
|
|
val = ad1843_set_recsrc(&devc->lith, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SOUND_MIXER_OUTSRC:
|
|
|
|
val = ad1843_set_outsrc(&devc->lith, val);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return -EINVAL;
|
|
|
|
}
|
|
|
|
if (val < 0)
|
|
|
|
return val;
|
|
|
|
return put_user(val, (int __user *) arg);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* This is the ioctl entry to the mixer driver. */
|
|
|
|
|
|
|
|
static int vwsnd_mixer_ioctl(struct inode *ioctl,
|
|
|
|
struct file *file,
|
|
|
|
unsigned int cmd,
|
|
|
|
unsigned long arg)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = (vwsnd_dev_t *) file->private_data;
|
|
|
|
const unsigned int nrmask = _IOC_NRMASK << _IOC_NRSHIFT;
|
|
|
|
const unsigned int nr = (cmd & nrmask) >> _IOC_NRSHIFT;
|
|
|
|
int retval;
|
|
|
|
|
|
|
|
DBGEV("(devc=0x%p, cmd=0x%x, arg=0x%lx)\n", devc, cmd, arg);
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_lock(&devc->mix_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
|
|
|
if ((cmd & ~nrmask) == MIXER_READ(0))
|
|
|
|
retval = mixer_read_ioctl(devc, nr, (void __user *) arg);
|
|
|
|
else if ((cmd & ~nrmask) == MIXER_WRITE(0))
|
|
|
|
retval = mixer_write_ioctl(devc, nr, (void __user *) arg);
|
|
|
|
else
|
|
|
|
retval = -EINVAL;
|
|
|
|
}
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_unlock(&devc->mix_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
return retval;
|
|
|
|
}
|
|
|
|
|
2007-02-12 16:55:37 +08:00
|
|
|
static const struct file_operations vwsnd_mixer_fops = {
|
2005-04-17 06:20:36 +08:00
|
|
|
.owner = THIS_MODULE,
|
|
|
|
.llseek = no_llseek,
|
|
|
|
.ioctl = vwsnd_mixer_ioctl,
|
|
|
|
.open = vwsnd_mixer_open,
|
|
|
|
.release = vwsnd_mixer_release,
|
|
|
|
};
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* probe/attach/unload */
|
|
|
|
|
|
|
|
/* driver probe routine. Return nonzero if hardware is found. */
|
|
|
|
|
|
|
|
static int __init probe_vwsnd(struct address_info *hw_config)
|
|
|
|
{
|
|
|
|
lithium_t lith;
|
|
|
|
int w;
|
|
|
|
unsigned long later;
|
|
|
|
|
|
|
|
DBGEV("(hw_config=0x%p)\n", hw_config);
|
|
|
|
|
|
|
|
/* XXX verify lithium present (to prevent crash on non-vw) */
|
|
|
|
|
|
|
|
if (li_create(&lith, hw_config->io_base) != 0) {
|
|
|
|
printk(KERN_WARNING "probe_vwsnd: can't map lithium\n");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
later = jiffies + 2;
|
|
|
|
li_writel(&lith, LI_HOST_CONTROLLER, LI_HC_LINK_ENABLE);
|
|
|
|
do {
|
|
|
|
w = li_readl(&lith, LI_HOST_CONTROLLER);
|
|
|
|
} while (w == LI_HC_LINK_ENABLE && time_before(jiffies, later));
|
|
|
|
|
|
|
|
li_destroy(&lith);
|
|
|
|
|
|
|
|
DBGPV("HC = 0x%04x\n", w);
|
|
|
|
|
|
|
|
if ((w == LI_HC_LINK_ENABLE) || (w & LI_HC_LINK_CODEC)) {
|
|
|
|
|
|
|
|
/* This may indicate a beta machine with no audio,
|
|
|
|
* or a future machine with different audio.
|
|
|
|
* On beta-release 320 w/ no audio, HC == 0x4000 */
|
|
|
|
|
|
|
|
printk(KERN_WARNING "probe_vwsnd: audio codec not found\n");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (w & LI_HC_LINK_FAILURE) {
|
|
|
|
printk(KERN_WARNING "probe_vwsnd: can't init audio codec\n");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
printk(KERN_INFO "vwsnd: lithium audio at mmio %#x irq %d\n",
|
|
|
|
hw_config->io_base, hw_config->irq);
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* driver attach routine. Initialize driver data structures and
|
|
|
|
* initialize hardware. A new vwsnd_dev_t is allocated and put
|
|
|
|
* onto the global list, vwsnd_dev_list.
|
|
|
|
*
|
|
|
|
* Return +minor_dev on success, -errno on failure.
|
|
|
|
*/
|
|
|
|
|
|
|
|
static int __init attach_vwsnd(struct address_info *hw_config)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc = NULL;
|
|
|
|
int err = -ENOMEM;
|
|
|
|
|
|
|
|
DBGEV("(hw_config=0x%p)\n", hw_config);
|
|
|
|
|
|
|
|
devc = kmalloc(sizeof (vwsnd_dev_t), GFP_KERNEL);
|
|
|
|
if (devc == NULL)
|
|
|
|
goto fail0;
|
|
|
|
|
|
|
|
err = li_create(&devc->lith, hw_config->io_base);
|
|
|
|
if (err)
|
|
|
|
goto fail1;
|
|
|
|
|
|
|
|
init_waitqueue_head(&devc->open_wait);
|
|
|
|
|
|
|
|
devc->rport.hwbuf_size = HWBUF_SIZE;
|
|
|
|
devc->rport.hwbuf_vaddr = __get_free_pages(GFP_KERNEL, HWBUF_ORDER);
|
|
|
|
if (!devc->rport.hwbuf_vaddr)
|
|
|
|
goto fail2;
|
|
|
|
devc->rport.hwbuf = (void *) devc->rport.hwbuf_vaddr;
|
|
|
|
devc->rport.hwbuf_paddr = virt_to_phys(devc->rport.hwbuf);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Quote from the NT driver:
|
|
|
|
*
|
|
|
|
* // WARNING!!! HACK to setup output dma!!!
|
|
|
|
* // This is required because even on output there is some data
|
|
|
|
* // trickling into the input DMA channel. This is a bug in the
|
|
|
|
* // Lithium microcode.
|
|
|
|
* // --sde
|
|
|
|
*
|
|
|
|
* We set the input side's DMA base address here. It will remain
|
|
|
|
* valid until the driver is unloaded.
|
|
|
|
*/
|
|
|
|
|
|
|
|
li_writel(&devc->lith, LI_COMM1_BASE,
|
|
|
|
devc->rport.hwbuf_paddr >> 8 | 1 << (37 - 8));
|
|
|
|
|
|
|
|
devc->wport.hwbuf_size = HWBUF_SIZE;
|
|
|
|
devc->wport.hwbuf_vaddr = __get_free_pages(GFP_KERNEL, HWBUF_ORDER);
|
|
|
|
if (!devc->wport.hwbuf_vaddr)
|
|
|
|
goto fail3;
|
|
|
|
devc->wport.hwbuf = (void *) devc->wport.hwbuf_vaddr;
|
|
|
|
devc->wport.hwbuf_paddr = virt_to_phys(devc->wport.hwbuf);
|
|
|
|
DBGP("wport hwbuf = 0x%p\n", devc->wport.hwbuf);
|
|
|
|
|
|
|
|
DBGDO(shut_up++);
|
|
|
|
err = ad1843_init(&devc->lith);
|
|
|
|
DBGDO(shut_up--);
|
|
|
|
if (err)
|
|
|
|
goto fail4;
|
|
|
|
|
|
|
|
/* install interrupt handler */
|
|
|
|
|
|
|
|
err = request_irq(hw_config->irq, vwsnd_audio_intr, 0, "vwsnd", devc);
|
|
|
|
if (err)
|
|
|
|
goto fail5;
|
|
|
|
|
|
|
|
/* register this device's drivers. */
|
|
|
|
|
|
|
|
devc->audio_minor = register_sound_dsp(&vwsnd_audio_fops, -1);
|
|
|
|
if ((err = devc->audio_minor) < 0) {
|
|
|
|
DBGDO(printk(KERN_WARNING
|
|
|
|
"attach_vwsnd: register_sound_dsp error %d\n",
|
|
|
|
err));
|
|
|
|
goto fail6;
|
|
|
|
}
|
|
|
|
devc->mixer_minor = register_sound_mixer(&vwsnd_mixer_fops,
|
|
|
|
devc->audio_minor >> 4);
|
|
|
|
if ((err = devc->mixer_minor) < 0) {
|
|
|
|
DBGDO(printk(KERN_WARNING
|
|
|
|
"attach_vwsnd: register_sound_mixer error %d\n",
|
|
|
|
err));
|
|
|
|
goto fail7;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Squirrel away device indices for unload routine. */
|
|
|
|
|
|
|
|
hw_config->slots[0] = devc->audio_minor;
|
|
|
|
|
|
|
|
/* Initialize as much of *devc as possible */
|
|
|
|
|
2006-03-23 19:00:39 +08:00
|
|
|
mutex_init(&devc->open_mutex);
|
|
|
|
mutex_init(&devc->io_mutex);
|
|
|
|
mutex_init(&devc->mix_mutex);
|
2005-04-17 06:20:36 +08:00
|
|
|
devc->open_mode = 0;
|
|
|
|
spin_lock_init(&devc->rport.lock);
|
|
|
|
init_waitqueue_head(&devc->rport.queue);
|
|
|
|
devc->rport.swstate = SW_OFF;
|
|
|
|
devc->rport.hwstate = HW_STOPPED;
|
|
|
|
devc->rport.flags = 0;
|
|
|
|
devc->rport.swbuf = NULL;
|
|
|
|
spin_lock_init(&devc->wport.lock);
|
|
|
|
init_waitqueue_head(&devc->wport.queue);
|
|
|
|
devc->wport.swstate = SW_OFF;
|
|
|
|
devc->wport.hwstate = HW_STOPPED;
|
|
|
|
devc->wport.flags = 0;
|
|
|
|
devc->wport.swbuf = NULL;
|
|
|
|
|
|
|
|
/* Success. Link us onto the local device list. */
|
|
|
|
|
|
|
|
devc->next_dev = vwsnd_dev_list;
|
|
|
|
vwsnd_dev_list = devc;
|
|
|
|
return devc->audio_minor;
|
|
|
|
|
|
|
|
/* So many ways to fail. Undo what we did. */
|
|
|
|
|
|
|
|
fail7:
|
|
|
|
unregister_sound_dsp(devc->audio_minor);
|
|
|
|
fail6:
|
|
|
|
free_irq(hw_config->irq, devc);
|
|
|
|
fail5:
|
|
|
|
fail4:
|
|
|
|
free_pages(devc->wport.hwbuf_vaddr, HWBUF_ORDER);
|
|
|
|
fail3:
|
|
|
|
free_pages(devc->rport.hwbuf_vaddr, HWBUF_ORDER);
|
|
|
|
fail2:
|
|
|
|
li_destroy(&devc->lith);
|
|
|
|
fail1:
|
|
|
|
kfree(devc);
|
|
|
|
fail0:
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int __exit unload_vwsnd(struct address_info *hw_config)
|
|
|
|
{
|
|
|
|
vwsnd_dev_t *devc, **devcp;
|
|
|
|
|
|
|
|
DBGE("()\n");
|
|
|
|
|
|
|
|
devcp = &vwsnd_dev_list;
|
|
|
|
while ((devc = *devcp)) {
|
|
|
|
if (devc->audio_minor == hw_config->slots[0]) {
|
|
|
|
*devcp = devc->next_dev;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
devcp = &devc->next_dev;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!devc)
|
|
|
|
return -ENODEV;
|
|
|
|
|
|
|
|
unregister_sound_mixer(devc->mixer_minor);
|
|
|
|
unregister_sound_dsp(devc->audio_minor);
|
|
|
|
free_irq(hw_config->irq, devc);
|
|
|
|
free_pages(devc->wport.hwbuf_vaddr, HWBUF_ORDER);
|
|
|
|
free_pages(devc->rport.hwbuf_vaddr, HWBUF_ORDER);
|
|
|
|
li_destroy(&devc->lith);
|
|
|
|
kfree(devc);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*****************************************************************************/
|
|
|
|
/* initialization and loadable kernel module interface */
|
|
|
|
|
|
|
|
static struct address_info the_hw_config = {
|
|
|
|
0xFF001000, /* lithium phys addr */
|
|
|
|
CO_IRQ(CO_APIC_LI_AUDIO) /* irq */
|
|
|
|
};
|
|
|
|
|
|
|
|
MODULE_DESCRIPTION("SGI Visual Workstation sound module");
|
|
|
|
MODULE_AUTHOR("Bob Miller <kbob@sgi.com>");
|
|
|
|
MODULE_LICENSE("GPL");
|
|
|
|
|
|
|
|
static int __init init_vwsnd(void)
|
|
|
|
{
|
|
|
|
int err;
|
|
|
|
|
|
|
|
DBGXV("\n");
|
|
|
|
DBGXV("sound::vwsnd::init_module()\n");
|
|
|
|
|
|
|
|
if (!probe_vwsnd(&the_hw_config))
|
|
|
|
return -ENODEV;
|
|
|
|
|
|
|
|
err = attach_vwsnd(&the_hw_config);
|
|
|
|
if (err < 0)
|
|
|
|
return err;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void __exit cleanup_vwsnd(void)
|
|
|
|
{
|
|
|
|
DBGX("sound::vwsnd::cleanup_module()\n");
|
|
|
|
|
|
|
|
unload_vwsnd(&the_hw_config);
|
|
|
|
}
|
|
|
|
|
|
|
|
module_init(init_vwsnd);
|
|
|
|
module_exit(cleanup_vwsnd);
|