git: 9front

ref: 902bd7ba274c50743a2cb15f4bca7a55b443172e
dir: /sys/src/9/pc/audioac97.c/

View raw version
#include "u.h"
#include "../port/lib.h"
#include "mem.h"
#include "dat.h"
#include "fns.h"
#include "io.h"
#include "../port/error.h"

typedef struct Hwdesc Hwdesc;
typedef struct Ctlr Ctlr;
static uint sis7012 = 0;

enum {
	Ioc	=	1<<31,
	Bup	=	1<<30,
};

struct Hwdesc {
	ulong addr;
	ulong size;
};

enum {
	Ndesc = 32,
	Nts = 33,
	Bufsize = 32768,	/* bytes, must be divisible by ndesc */
	Maxbusywait = 500000, /* microseconds, roughly */
	BytesPerSample = 4,
};

struct Ctlr {
	/* keep these first, they want to be 8-aligned */
	Hwdesc indesc[Ndesc];
	Hwdesc outdesc[Ndesc];
	Hwdesc micdesc[Ndesc];

	Lock;
	Rendez outr;

	ulong port;
	ulong mixport;

	char *out;
	char *in;
	char *mic;

	char *outp;
	char *inp;
	char *micp;

	/* shared variables, ilock to access */
	int outavail;
	int inavail;
	int micavail;

	/* interrupt handler alone */
	int outciv;
	int inciv;
	int micciv;

	int tsouti;
	uvlong tsoutp;
	ulong tsout[Nts];
	int tsoutb[Nts];

	ulong civstat[Ndesc];
	ulong lvistat[Ndesc];

	int targetrate;
	int hardrate;

	int attachok;
};

#define iorl(c, r)	(inl((c)->port+(r)))
#define iowl(c, r, l)	(outl((c)->port+(r), (ulong)(l)))

enum {
	In = 0x00,
	Out = 0x10,
	Mic = 0x20,
		Bar = 0x00,	/* Base address register, 8-byte aligned */
		/* a 32-bit read at 0x04 can be used to get civ:lvi:sr in one step */
		Civ = 0x04,	/* current index value (desc being processed) */
		Lvi = 0x05,	/* Last valid index (index of first unused entry!) */
		Sr = 0x06,	/* status register */
			Fifoe = 1<<4,	/* fifo error (r/wc) */
			Bcis = 1<<3,	/* buffer completion interrupt status (r/wc) */
			Lvbci = 1<<2,	/* last valid buffer completion(in)/fetched(out) interrupt (r/wc) */
			Celv = 1<<1,	/* current equals last valid (ro) */
			Dch = 1<<0,	/* dma controller halted (ro) */
		Picb = 0x08,	/* position in current buffer */
		Piv = 0x0a,	/* prefetched index value */
		Cr = 0x0b,	/* control register */
			Ioce = 1<<4,	/* interrupt on buffer completion (if bit set in hwdesc.size) (rw) */
			Feie = 1<<3,	/* fifo error interrupt enable (rw) */
			Lvbie = 1<<2,	/* last valid buffer interrupt enable (rw) */
			RR = 1<<1,	/* reset busmaster related regs, excl. ioce,feie,lvbie (rw) */
			Rpbm = 1<<0,	/* run/pause busmaster. 0 stops, 1 starts (rw) */
	Cnt = 0x2c,	/* global control */
		Ena16bit = 0x0<<22,
		Ena20bit = 0x1<<22,
		Ena2chan = 0x0<<20,
		Ena4chan = 0x1<<20,
		Enam6chan = 0x2<<20,
		EnaRESER = 0x3<<20,
		Sr2ie = 1<<6,	/* sdin2 interrupt enable (rw) */
		Srie = 1<<5,	/* sdin1 interrupt enable (rw) */
		Prie = 1<<4,	/* sdin0 interrupt enable (rw) */
		Aclso = 1<<3,	/* ac link shut-off (rw) */
		Acwr = 1<<2,	/* ac 97 warm reset (rw) */
		Accr = 1<<1,	/* ac 97 cold reset (rw) */
		GPIie = 1<<0,	/* GPI interrupt enable (rw) */
	Sta = 0x30,			/* global status */
		Cap6chan = 1<<21,
		Cap4chan = 1<<20,
		Md3 = 1<<17,	/* modem powerdown semaphore */
		Ad3 = 1<<16,	/* audio powerdown semaphore */
		Rcs = 1<<15,	/* read completion status (r/wc) */
		S2ri = 1<<29,	/* sdin2 resume interrupt (r/wc) */
		Sri = 1<<11,	/* sdin1 resume interrupt (r/wc) */
		Pri = 1<<10,	/* sdin0 resume interrupt (r/wc) */
		S2cr = 1<<28,	/* sdin2 codec ready (ro) */
		Scr = 1<<9,	/* sdin1 codec ready (ro) */
		Pcr = 1<<8,	/* sdin0 codec ready (ro) */
		Mint = 1<<7,	/* microphone in inetrrupt (ro) */
		Point = 1<<6,	/* pcm out interrupt (ro) */
		Piint = 1<<5,	/* pcm in interrupt (ro) */
		Moint = 1<<2,	/* modem out interrupt (ro) */
		Miint = 1<<1,	/* modem in interrupt (ro) */
		Gsci = 1<<0,	/* GPI status change interrupt */
	Cas = 0x34,	/* codec access semaphore */
		Casp = 1<<0,	/* set to 1 on read if zero, cleared by hardware */
};

#define csr8r(c, r)	(inb((c)->port+(r)))
#define csr16r(c, r)	(ins((c)->port+(r)))
#define csr32r(c, r)	(inl((c)->port+(r)))
#define csr8w(c, r, b)	(outb((c)->port+(r), (int)(b)))
#define csr16w(c, r, w)	(outs((c)->port+(r), (ushort)(w)))
#define csr32w(c, r, w)	(outl((c)->port+(r), (ulong)(w)))

/* audioac97mix */
extern int ac97hardrate(Audio *, int);
extern void ac97mixreset(Audio *, void (*wr)(Audio*,int,ushort), 
	ushort (*rr)(Audio*,int));

static void
ac97waitcodec(Audio *adev)
{
	Ctlr *ctlr;
	int i;
	ctlr = adev->ctlr;
	for(i = 0; i < Maxbusywait/10; i++){
		if((csr8r(ctlr, Cas) & Casp) == 0)
			break;
		microdelay(10);
	}
	if(i == Maxbusywait)
		print("#A%d: ac97 exhausted waiting codec access\n", adev->ctlrno);
}

static void
ac97mixw(Audio *adev, int port, ushort val)
{
	Ctlr *ctlr;
	ac97waitcodec(adev);
	ctlr = adev->ctlr;
	outs(ctlr->mixport+port, val);
}

static ushort
ac97mixr(Audio *adev, int port)
{
	Ctlr *ctlr;
	ac97waitcodec(adev);
	ctlr = adev->ctlr;
	return ins(ctlr->mixport+port);
}

static int
outavail(void *arg)
{
	Ctlr *ctlr;
	ctlr = arg;
	return ctlr->outavail > 0;
}

static void
ac97interrupt(Ureg *, void *arg)
{
	Audio *adev;
	Ctlr *ctlr;
	int civ, n, i;
	ulong stat;
	uvlong now;

	adev = arg;
	ctlr = adev->ctlr;
	stat = csr32r(ctlr, Sta);

	stat &= S2ri | Sri | Pri | Mint | Point | Piint | Moint | Miint | Gsci;

	ilock(ctlr);
	if(stat & Point){
		if(sis7012)
			csr16w(ctlr, Out + Picb, csr16r(ctlr, Out + Picb) & ~Dch);
		else
			csr16w(ctlr, Out + Sr, csr16r(ctlr, Out + Sr) & ~Dch);
		
		civ = csr8r(ctlr, Out + Civ);
		n = 0;
		while(ctlr->outciv != civ){
			ctlr->civstat[ctlr->outciv++]++;
			if(ctlr->outciv == Ndesc)
				ctlr->outciv = 0;
			n += Bufsize/Ndesc;
		}

		now = fastticks(0);
		i = ctlr->tsouti;
		ctlr->tsoutb[i] = n;
		ctlr->tsout[i] = now - ctlr->tsoutp;
		ctlr->tsouti = (i + 1) % Nts;
		ctlr->tsoutp = now;
		ctlr->outavail += n;
		
		if(ctlr->outavail > Bufsize/2)
			wakeup(&ctlr->outr);
		stat &= ~Point;	
	}
	iunlock(ctlr);
	if(stat) /* have seen 0x400, which is sdin0 resume */
		print("#A%d: ac97 unhandled interrupt(s): stat 0x%lux\n", adev->ctlrno, stat);
}

static int
off2lvi(char *base, char *p)
{
	int lvi;
	lvi = p - base;
	return lvi / (Bufsize/Ndesc);
}

static long
ac97medianoutrate(Audio *adev)
{
	ulong ts[Nts], t;
	uvlong hz;
	int i, j;
	Ctlr *ctlr;
	ctlr = adev->ctlr;
	fastticks(&hz);
	for(i = 0; i < Nts; i++)
		if(ctlr->tsout[i] > 0)
			ts[i] = (ctlr->tsoutb[i] * hz) / ctlr->tsout[i];
		else
			ts[i] = 0;
	for(i = 1; i < Nts; i++){
		t = ts[i];
		j = i;
		while(j > 0 && ts[j-1] > t){
			ts[j] = ts[j-1];
			j--;
		}
		ts[j] = t;
	}
	return ts[Nts/2] / BytesPerSample;
}

static void
ac97volume(Audio *adev, char *msg)
{
	adev->volwrite(adev, msg, strlen(msg), 0);
}

static void
ac97attach(Audio *adev)
{
	Ctlr *ctlr;
	ctlr = adev->ctlr;
	if(!ctlr->attachok){
		ac97hardrate(adev, ctlr->hardrate);
		ac97volume(adev, "audio 75");
		ac97volume(adev, "head 100");
		ac97volume(adev, "master 100");
		ctlr->attachok = 1;
	}
}

static long
ac97status(Audio *adev, void *a, long n, vlong off)
{
	Ctlr *ctlr;
	char *buf;
	long i, l;
	ctlr = adev->ctlr;
	l = 0;
	buf = malloc(READSTR);
	l += snprint(buf + l, READSTR - l, "rate %d\n", ctlr->targetrate);
	l += snprint(buf + l, READSTR - l, "median rate %lud\n", ac97medianoutrate(adev));
	l += snprint(buf + l, READSTR - l, "hard rate %d\n", ac97hardrate(adev, -1));

	l += snprint(buf + l, READSTR - l, "civ stats");
	for(i = 0; i < Ndesc; i++)
		l += snprint(buf + l, READSTR - l, " %lud", ctlr->civstat[i]);
	l += snprint(buf + l, READSTR - l, "\n");

	l += snprint(buf + l, READSTR - l, "lvi stats");
	for(i = 0; i < Ndesc; i++)
		l += snprint(buf + l, READSTR - l, " %lud", ctlr->lvistat[i]);
	snprint(buf + l, READSTR - l, "\n");

	n = readstr(off, a, n, buf);
	free(buf);
	return n;
}

static long
ac97buffered(Audio *adev)
{
	Ctlr *ctlr;
	ctlr = adev->ctlr;
	return Bufsize - Bufsize/Ndesc - ctlr->outavail;
}

static long
ac97ctl(Audio *adev, void *a, long n, vlong)
{
	Ctlr *ctlr;
	char *tok[2], *p;
	int ntok;
	long t;

	ctlr = adev->ctlr;
	if(n > READSTR)
		n = READSTR - 1;
	p = malloc(READSTR);

	if(waserror()){
		free(p);
		nexterror();
	}
	memmove(p, a, n);
	p[n] = 0;
	ntok = tokenize(p, tok, nelem(tok));
	if(ntok > 1 && !strcmp(tok[0], "rate")){
		t = strtol(tok[1], 0, 10);
		if(t < 8000 || t > 48000)
			error("rate must be between 8000 and 48000");
		ctlr->targetrate = t;
		ctlr->hardrate = t;
		ac97hardrate(adev, ctlr->hardrate);
		poperror();
		free(p);
		return n;
	}
	error("invalid ctl");
	return n; /* shut up, you compiler you */
}

static void
ac97kick(Ctlr *ctlr, long reg)
{
	csr8w(ctlr, reg+Cr, Ioce | Rpbm);
}

static long
ac97write(Audio *adev, void *a, long nwr, vlong)
{
	Ctlr *ctlr;
	char *p, *sp, *ep;
	int len, lvi, olvi;
	int t;
	long n;

	ctlr = adev->ctlr;
	ilock(ctlr);
	p = ctlr->outp;
	sp = a;
	ep = ctlr->out + Bufsize;
	olvi = off2lvi(ctlr->out, p);
	n = nwr;
	while(n > 0){
		len = ep - p;
		if(ctlr->outavail < len)
			len = ctlr->outavail;
		if(n < len)
			len = n;
		ctlr->outavail -= len;
		iunlock(ctlr);
		memmove(p, sp, len);
		ilock(ctlr);
		p += len;
		sp += len;
		n -= len;
		if(p == ep)
			p = ctlr->out;
		lvi = off2lvi(ctlr->out, p);
		if(olvi != lvi){
			t = olvi;
			while(t != lvi){
				t = (t + 1) % Ndesc;
				ctlr->lvistat[t]++;
				csr8w(ctlr, Out+Lvi, t);
				ac97kick(ctlr, Out);
			}
			olvi = lvi;
		}
		if(ctlr->outavail == 0){
			ctlr->outp = p;
			iunlock(ctlr);
			sleep(&ctlr->outr, outavail, ctlr);
			ilock(ctlr);
		}
	}
	ctlr->outp = p;
	iunlock(ctlr);
	return nwr;
}

static Pcidev*
ac97match(Pcidev *p)
{
	/* not all of the matched devices have been tested */
	while(p = pcimatch(p, 0, 0))
		switch(p->vid){
		default:
			break;
		case 0x1039:
			switch(p->did){
			default:
				break;
			case 0x7012:
				sis7012 = 1;
				return p;
			}
		case 0x1022:
			switch(p->did){
			default:
				break;
			case 0x746d:
			case 0x7445:
				return p;
			}
		case 0x10de:
			switch(p->did){
			default:
				break;
			case 0x01b1:
			case 0x006a:
			case 0x00da:
			case 0x00ea:
				return p;
			}
		case 0x8086:
			switch(p->did){
			default:
				break;
			case 0x2415:
			case 0x2425:
			case 0x2445:
			case 0x2485:
			case 0x24c5:
			case 0x24d5:
			case 0x25a6:
			case 0x266e:
			case 0x7195:
				return p;
			}
		}
	return nil;
}

static void
sethwp(Ctlr *ctlr, long off, void *ptr)
{
	csr8w(ctlr, off+Cr, RR);
	csr32w(ctlr, off+Bar, PCIWADDR(ptr));
	csr8w(ctlr, off+Lvi, 0);
}

static int
ac97reset(Audio *adev)
{
	static int ncards = 1;
	int i, irq, tbdf;
	Pcidev *p;
	Ctlr *ctlr;
	ulong ctl, stat = 0;

	p = nil;
	for(i = 0; i < ncards; i++)
		if((p = ac97match(p)) == nil)
			return -1;
	ncards++;

	ctlr = xspanalloc(sizeof(Ctlr), 8, 0);
	memset(ctlr, 0, sizeof(Ctlr));
	adev->ctlr = ctlr;
	ctlr->targetrate = 44100;
	ctlr->hardrate = 44100;

	if(p->mem[0].size == 64){
		ctlr->port = p->mem[0].bar & ~3;
		ctlr->mixport = p->mem[1].bar & ~3;
	} else if(p->mem[1].size == 64){
		ctlr->port = p->mem[1].bar & ~3;
		ctlr->mixport = p->mem[0].bar & ~3;
	} else if(p->mem[0].size == 256){			/* sis7012 */
		ctlr->port = p->mem[1].bar & ~3;
		ctlr->mixport = p->mem[0].bar & ~3;
	} else if(p->mem[1].size == 256){
		ctlr->port = p->mem[0].bar & ~3;
		ctlr->mixport = p->mem[1].bar & ~3;
	}

	irq = p->intl;
	tbdf = p->tbdf;

	print("#A%d: ac97 port 0x%04lux mixport 0x%04lux irq %d\n",
		adev->ctlrno, ctlr->port, ctlr->mixport, irq);

	pcisetbme(p);
	pcisetioe(p);

	ctlr->mic = xspanalloc(Bufsize, 8, 0);
	ctlr->in = xspanalloc(Bufsize, 8, 0);
	ctlr->out = xspanalloc(Bufsize, 8, 0);

	for(i = 0; i < Ndesc; i++){
		int size, off = i * (Bufsize/Ndesc);
		
		if(sis7012)
			size = (Bufsize/Ndesc);
		else
			size = (Bufsize/Ndesc) / 2;
		
		ctlr->micdesc[i].addr = PCIWADDR(ctlr->mic + off);
		ctlr->micdesc[i].size = Ioc | size;
		ctlr->indesc[i].addr = PCIWADDR(ctlr->in + off);
		ctlr->indesc[i].size = Ioc | size;
		ctlr->outdesc[i].addr = PCIWADDR(ctlr->out + off);
		ctlr->outdesc[i].size = Ioc | size;
	}

	ctlr->outavail = Bufsize - Bufsize/Ndesc;
	ctlr->outp = ctlr->out;

	ctl = csr32r(ctlr, Cnt);
	ctl &= ~(EnaRESER | Aclso);

	if((ctl & Accr) == 0){
		print("#A%d: ac97 cold reset\n", adev->ctlrno);
		ctl |= Accr;
	}else{
		print("#A%d: ac97 warm reset\n", adev->ctlrno);
		ctl |= Acwr;
	}

	csr32w(ctlr, Cnt, ctl);
	for(i = 0; i < Maxbusywait; i++){
		if((csr32r(ctlr, Cnt) & Acwr) == 0)
			break;
		microdelay(1);
	}
	if(i == Maxbusywait)
		print("#A%d: ac97 gave up waiting Acwr to go down\n", adev->ctlrno);

	for(i = 0; i < Maxbusywait; i++){
		if((stat = csr32r(ctlr, Sta)) & (Pcr | Scr | S2cr))
			break;
		microdelay(1);
	}
	if(i == Maxbusywait)
		print("#A%d: ac97 gave up waiting codecs become ready\n", adev->ctlrno);

	print("#A%d: ac97 codecs ready:%s%s%s\n", adev->ctlrno,
		(stat & Pcr) ? " sdin0" : "",
		(stat & Scr) ? " sdin1" : "",
		(stat & S2cr) ? " sdin2" : "");

	print("#A%d: ac97 codecs resumed:%s%s%s\n", adev->ctlrno,
		(stat & Pri) ? " sdin0" : "",
		(stat & Sri) ? " sdin1" : "",
		(stat & S2ri) ? " sdin2" : "");

	sethwp(ctlr, In, ctlr->indesc);
	sethwp(ctlr, Out, ctlr->outdesc);
	sethwp(ctlr, Mic, ctlr->micdesc);

	csr8w(ctlr, In+Cr, Ioce);	/*  | Lvbie | Feie */
	csr8w(ctlr, Out+Cr, Ioce);	/*  | Lvbie | Feie */
	csr8w(ctlr, Mic+Cr, Ioce);	/*  | Lvbie | Feie */

	adev->attach = ac97attach;
	adev->write = ac97write;
	adev->status = ac97status;
	adev->ctl = ac97ctl;
	adev->buffered = ac97buffered;
	
	ac97mixreset(adev, ac97mixw, ac97mixr);

	intrenable(irq, ac97interrupt, adev, tbdf, adev->name);

	return 0;
}

void
audioac97link(void)
{
	addaudiocard("ac97audio", ac97reset);
}