/* $NetBSD: vac.c,v 1.1 2026/06/18 00:44:21 rkujawa Exp $ */

/*-
 * Copyright (c) 2026 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Driver for the SM502 AC'97 audio interface
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: vac.c,v 1.1 2026/06/18 00:44:21 rkujawa Exp $");

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/kmem.h>
#include <sys/bus.h>
#include <sys/callout.h>
#include <sys/sysctl.h>
#include <sys/audioio.h>

#include <dev/audio/audio_if.h>

#include <dev/ic/ac97var.h>
#include <dev/ic/ac97reg.h>

#include <dev/ic/sm502reg.h>
#include <dev/pci/voyagervar.h>

/* PCM playback format: AC'97 fixed 48 kHz, 16-bit, stereo. */
#define	VAC_RATE	48000
#define	VAC_CHANNELS	2
#define	VAC_PRECISION	16
#define	VAC_FRAMESIZE	(VAC_CHANNELS * (VAC_PRECISION / 8))	/* 4 bytes */

/* poll when waiting on the codec */
#define	VAC_CODEC_TIMEOUT	1000

/*
 * 8051 polled protocol, via 4KB dual-port SRAM 
 *
 * Two PCM buffers selected by command parity
 */
#define	VAC_DP_BASE		SM502_uC_SRAM_DUALPORT
#define	VAC_DP_H_CMD		0x00	/* u8  host: command sequence */
#define	VAC_DP_H_DONE		0x01	/* u8  8051: last finished command */
#define	VAC_DP_H_ALIVE		0x03	/* u8  8051: VAC_DP_ALIVE_MAGIC when up */
#define	VAC_DP_H_FC0L		0x04	/* u16 host: buf0 frame count, LE */
#define	VAC_DP_H_FC0H		0x05
#define	VAC_DP_H_FC1L		0x06	/* u16 host: buf1 frame count, LE */
#define	VAC_DP_H_FC1H		0x07
#define	VAC_DP_BUF0		0x10
#define	VAC_DP_MAXFRAMES	504	/* per buffer; 504*4=0x7e0, both fit 4KB */
#define	VAC_DP_BUFBYTES		(VAC_DP_MAXFRAMES * VAC_FRAMESIZE)
#define	VAC_DP_BUF1		(VAC_DP_BUF0 + VAC_DP_BUFBYTES)	/* 0x7f0 */
#define	VAC_DP_ALIVE_MAGIC	0x51

struct vac_softc {
	device_t		sc_dev;
	bus_space_tag_t		sc_memt;
	bus_space_handle_t	sc_regh;	/* BAR 0x14: MMIO registers */
	void			*sc_parent;	/* voyager softc, for gpio/power */

	kmutex_t		sc_lock;	/* thread context */
	kmutex_t		sc_intr_lock;	/* "interrupt" context */

	struct ac97_host_if	sc_ac97_host;
	struct ac97_codec_if	*sc_ac97_codec;
	device_t		sc_audiodev;

	/* MI playback ring (filled by the audio layer, consumed by the feeder) */
	bool			sc_active;	/* playback in progress */
	uint8_t			*sc_pstart;
	uint8_t			*sc_pend;
	uint8_t			*sc_pcur;
	int			sc_pblksize;
	void			(*sc_pint)(void *);
	void			*sc_pintarg;

	/* 8051 interface */
	callout_t		sc_feed_ch;	/* polls H_DONE, refills buffers */
	int			sc_feed_ticks;	/* callout period */
	uint8_t			*sc_dpstage;	/* staging for one dual-port buf */
	uint8_t			sc_cmd_seq;	/* last command we queued */
	uint8_t			sc_done_seen;	/* last H_DONE we acted on */
	bool			sc_8051_ok;	/* feeder firmware responded */
	int			sc_underruns;	/* 8051 ran dry while playing */

	struct sysctllog	*sc_sysctllog;
};

#define	VAC_READ(sc, reg)		\
	bus_space_read_4((sc)->sc_memt, (sc)->sc_regh, (reg))
#define	VAC_WRITE(sc, reg, val)		\
	bus_space_write_4((sc)->sc_memt, (sc)->sc_regh, (reg), (val))
#define	VAC_DP_R8(sc, off) \
	bus_space_read_1((sc)->sc_memt, (sc)->sc_regh, VAC_DP_BASE + (off))
#define	VAC_DP_W8(sc, off, v) \
	bus_space_write_1((sc)->sc_memt, (sc)->sc_regh, VAC_DP_BASE + (off), (v))

/*
 * Embedded 8051 firmware
 * assemble from vac8051.asm with naken_asm (-type bin)
 */
static const uint8_t vac8051_fw[] = {
	0x90, 0x30, 0x03, 0x74, 0x51, 0xf0, 0x90, 0x30, 0x01, 0xe4, 0xf0, 0x7f,
	0x00, 0x90, 0x30, 0x00, 0xe0, 0xfe, 0x6f, 0x60, 0xf8, 0xee, 0x54, 0x01,
	0x60, 0x10, 0x90, 0x30, 0x06, 0xe0, 0xfc, 0x90, 0x30, 0x07, 0xe0, 0xfd,
	0x7a, 0xf0, 0x7b, 0x37, 0x80, 0x0e, 0x90, 0x30, 0x04, 0xe0, 0xfc, 0x90,
	0x30, 0x05, 0xe0, 0xfd, 0x7a, 0x10, 0x7b, 0x30, 0x78, 0x00, 0x79, 0x00,
	0x90, 0x91, 0x45, 0xe0, 0x54, 0x08, 0x60, 0x04, 0xd9, 0xf6, 0xd8, 0xf4,
	0x8a, 0x82, 0x8b, 0x83, 0xe0, 0xf5, 0x20, 0xa3, 0xe0, 0xf5, 0x21, 0xa3,
	0xe0, 0xf5, 0x24, 0xa3, 0xe0, 0xf5, 0x25, 0xa3, 0xaa, 0x82, 0xab, 0x83,
	0x90, 0x91, 0x00, 0xe4, 0xf0, 0xa3, 0x74, 0x98, 0xf0, 0xe5, 0x20, 0xc4,
	0xf5, 0x22, 0xe5, 0x21, 0xc4, 0xf5, 0x23, 0x90, 0x91, 0x0c, 0xe5, 0x22,
	0x54, 0xf0, 0xf0, 0xa3, 0xe5, 0x22, 0x54, 0x0f, 0xf5, 0x26, 0xe5, 0x23,
	0x54, 0xf0, 0x45, 0x26, 0xf0, 0xa3, 0xe5, 0x23, 0x54, 0x0f, 0xf0, 0xa3,
	0xe4, 0xf0, 0xe5, 0x24, 0xc4, 0xf5, 0x22, 0xe5, 0x25, 0xc4, 0xf5, 0x23,
	0x90, 0x91, 0x10, 0xe5, 0x22, 0x54, 0xf0, 0xf0, 0xa3, 0xe5, 0x22, 0x54,
	0x0f, 0xf5, 0x26, 0xe5, 0x23, 0x54, 0xf0, 0x45, 0x26, 0xf0, 0xa3, 0xe5,
	0x23, 0x54, 0x0f, 0xf0, 0xa3, 0xe4, 0xf0, 0xec, 0x70, 0x01, 0x1d, 0x1c,
	0xec, 0x4d, 0x60, 0x03, 0x02, 0x00, 0x38, 0x90, 0x30, 0x01, 0xee, 0xf0,
	0xff, 0x02, 0x00, 0x0d
};

static int	vac_match(device_t, cfdata_t, void *);
static void	vac_attach(device_t, device_t, void *);

CFATTACH_DECL_NEW(vac, sizeof(struct vac_softc),
    vac_match, vac_attach, NULL, NULL);

static const struct audio_format vac_format = {
	.mode = AUMODE_PLAY,
	.encoding = AUDIO_ENCODING_SLINEAR_LE,
	.validbits = VAC_PRECISION,
	.precision = VAC_PRECISION,
	.channels = VAC_CHANNELS,
	.channel_mask = AUFMT_STEREO,
	.frequency_type = 1,
	.frequency = { VAC_RATE }
};

/*
 * AC'97 codec register access.
 */
static int
vac_ac97_read(void *priv, uint8_t reg, uint16_t *val)
{
	struct vac_softc * const sc = priv;
	uint32_t addr, rx;
	int i;

	addr = ((uint32_t)reg << SM502_AC97_ADDR_SHIFT) & SM502_AC97_ADDR_MASK;

	/* Mark frame + slot 1 valid and issue a read of the register. */
	VAC_WRITE(sc, SM502_AC97_TX_TAG,
	    SM502_AC97_FRAME_VALID | SM502_AC97_S1_VALID);
	VAC_WRITE(sc, SM502_AC97_TX_ADDR, addr | SM502_AC97_READ);

	/* Wait for the codec to echo the register index back in RX slot 1. */
	for (i = 0; i < VAC_CODEC_TIMEOUT; i++) {
		rx = VAC_READ(sc, SM502_AC97_RX_ADDR);
		if ((rx & SM502_AC97_ADDR_MASK) == addr)
			break;
		DELAY(1);
	}
	if (i == VAC_CODEC_TIMEOUT)
		return 1;

	*val = (VAC_READ(sc, SM502_AC97_RX_DATA) & SM502_AC97_DATA_MASK) >> 4;
	return 0;
}

static int
vac_ac97_write(void *priv, uint8_t reg, uint16_t val)
{
	struct vac_softc * const sc = priv;
	uint32_t addr;

	addr = ((uint32_t)reg << SM502_AC97_ADDR_SHIFT) & SM502_AC97_ADDR_MASK;

	/* Mark frame + slots 1 (address) and 2 (data) valid. */
	VAC_WRITE(sc, SM502_AC97_TX_TAG,
	    SM502_AC97_FRAME_VALID | SM502_AC97_S1_VALID | SM502_AC97_S2_VALID);
	VAC_WRITE(sc, SM502_AC97_TX_DATA,
	    ((uint32_t)val << 4) & SM502_AC97_DATA_MASK);
	VAC_WRITE(sc, SM502_AC97_TX_ADDR, addr);

	DELAY(100);
	return 0;
}

static int
vac_ac97_reset(void *priv)
{
	struct vac_softc * const sc = priv;
	uint32_t reg;
	int i;

	aprint_debug_dev(sc->sc_dev, "ac97 control at reset entry 0x%08x\n",
	    VAC_READ(sc, SM502_AC97_CONTROL));

	/*
	 * Cold reset the codec with the controller enabled: assert RESET#
	 * (COLD_RESET) for well over the 1us minimum, then release it.
	 */
	VAC_WRITE(sc, SM502_AC97_CONTROL,
	    SM502_AC97_ENABLE | SM502_AC97_COLD_RESET);
	DELAY(20);
	VAC_WRITE(sc, SM502_AC97_CONTROL, SM502_AC97_ENABLE);

	/*
	 * Wait (up to ~250ms) for the codec to start the bit clock and the
	 * link to reach the active state.
	 */
	for (i = 0; i < 2500; i++) {
		reg = VAC_READ(sc, SM502_AC97_CONTROL);
		if ((reg & SM502_AC97_BCLK_RUNNING) != 0 &&
		    (reg & SM502_AC97_STATUS_MASK) == SM502_AC97_STATUS_ON) {
			aprint_debug_dev(sc->sc_dev,
			    "AC-link up (control 0x%08x)\n", reg);
			return 0;
		}
		DELAY(100);
	}

	aprint_error_dev(sc->sc_dev,
	    "AC-link did not come ready (control 0x%08x)\n", reg);
	return 1;
}

static int
vac_ac97_attach(void *priv, struct ac97_codec_if *codec)
{
	struct vac_softc * const sc = priv;

	sc->sc_ac97_codec = codec;
	return 0;
}

static void
vac_unmute(struct vac_softc *sc)
{
	struct ac97_codec_if * const codec = sc->sc_ac97_codec;
	static const struct {
		const char *class;
		const char *device;
	} ports[] = {
		{ AudioCoutputs, AudioNmaster },
		{ AudioCinputs,  AudioNdac },
	};
	mixer_ctrl_t mc;
	int i, idx;

	mutex_enter(&sc->sc_lock);
	for (i = 0; i < (int)__arraycount(ports); i++) {
		idx = codec->vtbl->get_portnum_by_name(codec,
		    ports[i].class, ports[i].device, NULL);
		if (idx >= 0) {
			memset(&mc, 0, sizeof(mc));
			mc.dev = idx;
			mc.type = AUDIO_MIXER_VALUE;
			mc.un.value.num_channels = 2;
			mc.un.value.level[AUDIO_MIXER_LEVEL_LEFT] = 200;
			mc.un.value.level[AUDIO_MIXER_LEVEL_RIGHT] = 200;
			(void)codec->vtbl->mixer_set_port(codec, &mc);
		}
		idx = codec->vtbl->get_portnum_by_name(codec,
		    ports[i].class, ports[i].device, AudioNmute);
		if (idx >= 0) {
			memset(&mc, 0, sizeof(mc));
			mc.dev = idx;
			mc.type = AUDIO_MIXER_ENUM;
			mc.un.ord = 0;		/* off = not muted */
			(void)codec->vtbl->mixer_set_port(codec, &mc);
		}
	}
	mutex_exit(&sc->sc_lock);
}

/*
 * MI audio interface.
 */

static int
vac_query_format(void *priv, audio_format_query_t *afp)
{

	return audio_query_format(&vac_format, 1, afp);
}

static int
vac_set_format(void *priv, int setmode,
    const audio_params_t *play, const audio_params_t *rec,
    audio_filter_reg_t *pfil, audio_filter_reg_t *rfil)
{

	/* Only one format; nothing to do. */
	return 0;
}

static int
vac_getdev(void *priv, struct audio_device *ad)
{

	snprintf(ad->name, sizeof(ad->name), "SM502");
	snprintf(ad->version, sizeof(ad->version), "AC97");
	snprintf(ad->config, sizeof(ad->config), "vac");
	return 0;
}

static int
vac_set_port(void *priv, mixer_ctrl_t *mc)
{
	struct vac_softc * const sc = priv;

	return sc->sc_ac97_codec->vtbl->mixer_set_port(sc->sc_ac97_codec, mc);
}

static int
vac_get_port(void *priv, mixer_ctrl_t *mc)
{
	struct vac_softc * const sc = priv;

	return sc->sc_ac97_codec->vtbl->mixer_get_port(sc->sc_ac97_codec, mc);
}

static int
vac_query_devinfo(void *priv, mixer_devinfo_t *di)
{
	struct vac_softc * const sc = priv;

	return sc->sc_ac97_codec->vtbl->query_devinfo(sc->sc_ac97_codec, di);
}

static int
vac_get_props(void *priv)
{

	return AUDIO_PROP_PLAYBACK;
}

static int
vac_round_blocksize(void *priv, int blk, int mode,
    const audio_params_t *param)
{

	/*
	 * Play one MI block per dual-port buffer
	 */
	if (blk > VAC_DP_BUFBYTES)
		blk = VAC_DP_BUFBYTES;
	blk &= ~(VAC_FRAMESIZE - 1);
	if (blk < VAC_FRAMESIZE)
		blk = VAC_FRAMESIZE;
	return blk;
}

/*
 * Queue one block from the MI ring 
 */
static void
vac_dp_queue(struct vac_softc *sc)
{
	const uint8_t cmd = sc->sc_cmd_seq + 1;
	uint8_t *p = sc->sc_pcur;
	bus_size_t base;
	int n, i;

	n = sc->sc_pblksize / VAC_FRAMESIZE;
	if (n > VAC_DP_MAXFRAMES)
		n = VAC_DP_MAXFRAMES;

	/* Copy PCM into the staging buffer (raw 16-bit LE), handling wrap. */
	for (i = 0; i < n; i++) {
		uint8_t * const d = sc->sc_dpstage + i * VAC_FRAMESIZE;

		d[0] = p[0];	/* left low */
		d[1] = p[1];	/* left high */
		d[2] = p[2];	/* right low */
		d[3] = p[3];	/* right high */
		p += VAC_FRAMESIZE;
		if (p >= sc->sc_pend)
			p = sc->sc_pstart;
	}
	sc->sc_pcur = p;

	base = VAC_DP_BASE + ((cmd & 1) ? VAC_DP_BUF1 : VAC_DP_BUF0);
	bus_space_write_region_1(sc->sc_memt, sc->sc_regh, base,
	    sc->sc_dpstage, n * VAC_FRAMESIZE);

	if (cmd & 1) {
		VAC_DP_W8(sc, VAC_DP_H_FC1L, n & 0xff);
		VAC_DP_W8(sc, VAC_DP_H_FC1H, (n >> 8) & 0xff);
	} else {
		VAC_DP_W8(sc, VAC_DP_H_FC0L, n & 0xff);
		VAC_DP_W8(sc, VAC_DP_H_FC0H, (n >> 8) & 0xff);
	}

	/* The data and count must be visible before the command bump. */
	bus_space_barrier(sc->sc_memt, sc->sc_regh, VAC_DP_BASE, 0x1000,
	    BUS_SPACE_BARRIER_WRITE);
	VAC_DP_W8(sc, VAC_DP_H_CMD, cmd);
	sc->sc_cmd_seq = cmd;
}

/*
 * Poll the 8051's done counter
 */
static void
vac_feed_callout(void *arg)
{
	struct vac_softc * const sc = arg;
	uint8_t done;
	int n;

	mutex_enter(&sc->sc_intr_lock);
	if (!sc->sc_active) {
		mutex_exit(&sc->sc_intr_lock);
		return;
	}

	done = VAC_DP_R8(sc, VAC_DP_H_DONE);
	if (sc->sc_cmd_seq == done)
		sc->sc_underruns++;	/* 8051 caught up: nothing queued */

	n = (uint8_t)(done - sc->sc_done_seen);
	sc->sc_done_seen = done;
	while (n-- > 0 && sc->sc_active && sc->sc_pint != NULL)
		sc->sc_pint(sc->sc_pintarg);

	while ((uint8_t)(sc->sc_cmd_seq - done) <= 1 && sc->sc_active)
		vac_dp_queue(sc);

	callout_schedule(&sc->sc_feed_ch, sc->sc_feed_ticks);
	mutex_exit(&sc->sc_intr_lock);
}

static int
vac_trigger_output(void *priv, void *start, void *end, int blksize,
    void (*intr)(void *), void *intrarg, const audio_params_t *params)
{
	struct vac_softc * const sc = priv;
	int i;

	if (!sc->sc_8051_ok)
		return ENXIO;

	/*
	 * Restart the 8051 cleanly for this playback
	 */
	VAC_WRITE(sc, SM502_uC_RESET, 0);
	for (i = 0; i < 16; i++)
		VAC_DP_W8(sc, i, 0);
	VAC_WRITE(sc, SM502_uC_RESET, SM502_uC_ENABLE);
	for (i = 0; i < 5000 &&
	    VAC_DP_R8(sc, VAC_DP_H_ALIVE) != VAC_DP_ALIVE_MAGIC; i++)
		DELAY(10);

	mutex_enter(&sc->sc_intr_lock);
	sc->sc_pstart = start;
	sc->sc_pend = end;
	sc->sc_pcur = start;
	sc->sc_pblksize = blksize;
	sc->sc_pint = intr;
	sc->sc_pintarg = intrarg;

	sc->sc_cmd_seq = 0;
	sc->sc_done_seen = 0;
	sc->sc_underruns = 0;
	sc->sc_active = true;

	/* Prime both buffers, then poll for completion from the callout. */
	vac_dp_queue(sc);
	vac_dp_queue(sc);
	callout_schedule(&sc->sc_feed_ch, sc->sc_feed_ticks);
	mutex_exit(&sc->sc_intr_lock);

	return 0;
}

static int
vac_halt_output(void *priv)
{
	struct vac_softc * const sc = priv;

	mutex_enter(&sc->sc_intr_lock);
	sc->sc_active = false;
	sc->sc_pint = NULL;
	mutex_exit(&sc->sc_intr_lock);

	callout_halt(&sc->sc_feed_ch, NULL);

	/* Park the 8051 so the host codec/mixer path is live while idle. */
	VAC_WRITE(sc, SM502_uC_RESET, 0);
	return 0;
}

static void
vac_get_locks(void *priv, kmutex_t **intr, kmutex_t **thread)
{
	struct vac_softc * const sc = priv;

	*intr = &sc->sc_intr_lock;
	*thread = &sc->sc_lock;
}

static const struct audio_hw_if vac_hw_if = {
	.query_format = vac_query_format,
	.set_format = vac_set_format,
	.getdev = vac_getdev,
	.set_port = vac_set_port,
	.get_port = vac_get_port,
	.query_devinfo = vac_query_devinfo,
	.get_props = vac_get_props,
	.round_blocksize = vac_round_blocksize,
	.trigger_output = vac_trigger_output,
	.halt_output = vac_halt_output,
	.get_locks = vac_get_locks,
};

/* Load the firmware blob as 32-bit LE words at (h, base). */
static void
vac_load_fw(struct vac_softc *sc, bus_space_handle_t h, bus_size_t base)
{
	size_t i;

	for (i = 0; i < sizeof(vac8051_fw); i += 4) {
		uint8_t b0 = vac8051_fw[i];
		uint8_t b1 = (i + 1 < sizeof(vac8051_fw)) ? vac8051_fw[i + 1] : 0;
		uint8_t b2 = (i + 2 < sizeof(vac8051_fw)) ? vac8051_fw[i + 2] : 0;
		uint8_t b3 = (i + 3 < sizeof(vac8051_fw)) ? vac8051_fw[i + 3] : 0;

		bus_space_write_4(sc->sc_memt, h, base + i,
		    b0 | (b1 << 8) | (b2 << 16) | (b3 << 24));
	}
}

static void
vac_8051_bringup(struct vac_softc *sc)
{
	uint32_t v;
	uint8_t alive;
	int i;

	/* Hold the 8051 in reset... mode = SRAM on, AC97, fastest clock (div2). */
	VAC_WRITE(sc, SM502_uC_RESET, 0);
	v = VAC_READ(sc, SM502_uC_MODE_SELECT);
	v &= ~(SM502_uC_SRAM_DISABLE | SM502_uC_CODEC_I2S | SM502_uC_CLOCK_MASK);
	VAC_WRITE(sc, SM502_uC_MODE_SELECT, v);

	/* Load the feeder into the program SRAM (host R/W only while in reset). */
	vac_load_fw(sc, sc->sc_regh, SM502_uC_SRAM_PROG);

	/* Clear the dual-port protocol header before the 8051 starts. */
	for (i = 0; i < 16; i++)
		VAC_DP_W8(sc, i, 0);

	/* Release the 8051 and give it time to post its alive marker. */
	VAC_WRITE(sc, SM502_uC_RESET, SM502_uC_ENABLE);
	DELAY(2000);

	alive = VAC_DP_R8(sc, VAC_DP_H_ALIVE);
	sc->sc_8051_ok = (alive == VAC_DP_ALIVE_MAGIC);
	aprint_verbose_dev(sc->sc_dev, "8051 feeder %s (alive=0x%02x)\n",
	    sc->sc_8051_ok ? "loaded" : "not responding", alive);
	if (!sc->sc_8051_ok)
		aprint_error_dev(sc->sc_dev,
		    "8051 feeder did not start; playback unavailable\n");

	/* Park the 8051 while idle (keeps the host codec/mixer path live). */
	VAC_WRITE(sc, SM502_uC_RESET, 0);
}

#ifdef VAC_DEBUG
/*
 * Diagnostics: dump the AC97 link control + key codec registers (read while the
 * 8051 is idle; codec access is dead while it runs).
 */
static int
vac_sysctl_diag(SYSCTLFN_ARGS)
{
	struct sysctlnode node = *rnode;
	struct vac_softc * const sc = node.sysctl_data;
	uint16_t r02 = 0, r04 = 0, r18 = 0, r26 = 0, r2a = 0, r2c = 0, r7a = 0;
	char buf[200];

	vac_ac97_read(sc, 0x02, &r02);	/* master volume */
	vac_ac97_read(sc, 0x04, &r04);	/* aux/headphone volume */
	vac_ac97_read(sc, 0x18, &r18);	/* PCM out volume */
	vac_ac97_read(sc, 0x26, &r26);	/* powerdown ctrl/status */
	vac_ac97_read(sc, 0x2a, &r2a);	/* extended audio ctrl (VRA) */
	vac_ac97_read(sc, 0x2c, &r2c);	/* PCM front DAC rate */
	vac_ac97_read(sc, 0x7a, &r7a);	/* ALC655 vendor (EAPD etc.) */

	snprintf(buf, sizeof(buf),
	    "acctl=%08x mode=%08x reset=%08x | "
	    "m02=%04x hp04=%04x pcm18=%04x pwr26=%04x ext2a=%04x rate2c=%04x ven7a=%04x",
	    VAC_READ(sc, SM502_AC97_CONTROL),
	    VAC_READ(sc, SM502_uC_MODE_SELECT),
	    VAC_READ(sc, SM502_uC_RESET),
	    r02, r04, r18, r26, r2a, r2c, r7a);
	node.sysctl_data = buf;
	node.sysctl_size = strlen(buf) + 1;
	return sysctl_lookup(SYSCTLFN_CALL(&node));
}
#endif /* VAC_DEBUG */

static void
vac_sysctl_setup(struct vac_softc *sc)
{
	const struct sysctlnode *node = NULL;

	if (sysctl_createv(&sc->sc_sysctllog, 0, NULL, &node,
	    0, CTLTYPE_NODE, "vac", SYSCTL_DESCR("SM502 AC97-link audio"),
	    NULL, 0, NULL, 0, CTL_HW, CTL_CREATE, CTL_EOL) != 0 || node == NULL)
		return;

	sysctl_createv(&sc->sc_sysctllog, 0, NULL, NULL,
	    CTLFLAG_READONLY, CTLTYPE_INT, "underruns",
	    SYSCTL_DESCR("times the 8051 ran dry during the last playback"),
	    NULL, 0, &sc->sc_underruns, 0, CTL_HW, node->sysctl_num,
	    CTL_CREATE, CTL_EOL);
#ifdef VAC_DEBUG
	sysctl_createv(&sc->sc_sysctllog, 0, NULL, NULL,
	    CTLFLAG_READONLY, CTLTYPE_STRING, "diag",
	    SYSCTL_DESCR("AC97 link + codec register dump (read while idle)"),
	    vac_sysctl_diag, 0, (void *)sc, 0, CTL_HW, node->sysctl_num,
	    CTL_CREATE, CTL_EOL);
#endif
}

static int
vac_match(device_t parent, cfdata_t match, void *aux)
{
	struct voyager_attach_args *vaa = aux;

	if (strcmp(vaa->vaa_name, "vac") == 0)
		return 100;
	return 0;
}

static void
vac_attach(device_t parent, device_t self, void *aux)
{
	struct vac_softc * const sc = device_private(self);
	struct voyager_attach_args *vaa = aux;
	int error;

	sc->sc_dev = self;
	sc->sc_memt = vaa->vaa_tag;
	sc->sc_regh = vaa->vaa_regh;
	sc->sc_parent = device_private(parent);

	aprint_naive("\n");
	aprint_normal(": SM502 AC97-link\n");

	mutex_init(&sc->sc_lock, MUTEX_DEFAULT, IPL_NONE);
	mutex_init(&sc->sc_intr_lock, MUTEX_DEFAULT, IPL_AUDIO);

	callout_init(&sc->sc_feed_ch, CALLOUT_MPSAFE);
	callout_setfunc(&sc->sc_feed_ch, vac_feed_callout, sc);
	sc->sc_feed_ticks = mstohz(5);
	if (sc->sc_feed_ticks < 1)
		sc->sc_feed_ticks = 1;
	sc->sc_dpstage = kmem_alloc(VAC_DP_BUFBYTES, KM_SLEEP);

	aprint_debug_dev(self,
	    "power: ctrl 0x%08x cur-gate 0x%08x gate0 0x%08x gate1 0x%08x\n",
	    VAC_READ(sc, SM502_POWER_MODE_CONTROL),
	    VAC_READ(sc, SM502_CURRENT_GATE),
	    VAC_READ(sc, SM502_POWER_MODE0_GATE),
	    VAC_READ(sc, SM502_POWER_MODE1_GATE));

	VAC_WRITE(sc, SM502_POWER_MODE0_GATE,
	    VAC_READ(sc, SM502_POWER_MODE0_GATE) |
	    SM502_GATE_AUDIO_ENABLE | SM502_GATE_8051_ENABLE);
	VAC_WRITE(sc, SM502_POWER_MODE1_GATE,
	    VAC_READ(sc, SM502_POWER_MODE1_GATE) |
	    SM502_GATE_AUDIO_ENABLE | SM502_GATE_8051_ENABLE);
	DELAY(1000);

	/* Route the shared audio pins to the AC97-link (not GPIO). */
	voyager_control_gpio(sc->sc_parent, 0xffffffff, SM502_AUDIO_GPIO_MASK);

	/* Select AC97 mode for the link (CODEC_I2S clear). */
	VAC_WRITE(sc, SM502_uC_MODE_SELECT,
	    VAC_READ(sc, SM502_uC_MODE_SELECT) & ~SM502_uC_CODEC_I2S);

	sc->sc_ac97_host.arg = sc;
	sc->sc_ac97_host.attach = vac_ac97_attach;
	sc->sc_ac97_host.read = vac_ac97_read;
	sc->sc_ac97_host.write = vac_ac97_write;
	sc->sc_ac97_host.reset = vac_ac97_reset;

	error = ac97_attach(&sc->sc_ac97_host, self, &sc->sc_lock);
	if (error) {
		aprint_error_dev(self, "couldn't attach codec (%d)\n", error);
		return;
	}

	vac_sysctl_setup(sc);

	/* Load the 8051 binary. */
	vac_8051_bringup(sc);

	sc->sc_audiodev = audio_attach_mi(&vac_hw_if, sc, self);

	/* Unmute master + DAC now that the codec/mixer stack is up (8051 idle). */
	vac_unmute(sc);
}
