root/sys/dev/hdaudio/hdaudio.c
/* $NetBSD: hdaudio.c,v 1.18 2022/04/07 19:33:37 andvar Exp $ */

/*
 * Copyright (c) 2009 Precedence Technologies Ltd <support@precedence.co.uk>
 * Copyright (c) 2009 Jared D. McNeill <jmcneill@invisible.ca>
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Precedence Technologies Ltd
 *
 * 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. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: hdaudio.c,v 1.18 2022/04/07 19:33:37 andvar Exp $");

#include <sys/types.h>
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/conf.h>
#include <sys/bus.h>
#include <sys/kmem.h>
#include <sys/module.h>

#include "hdaudiovar.h"
#include "hdaudioreg.h"
#include "hdaudioio.h"
#include "hdaudio_verbose.h"
#include "hdaudiodevs.h"

/* #define      HDAUDIO_DEBUG */

#define HDAUDIO_RESET_TIMEOUT   5000
#define HDAUDIO_CORB_TIMEOUT    1000
#define HDAUDIO_RIRB_TIMEOUT    5000

#define HDAUDIO_CODEC_DELAY     1000    /* spec calls for 250 */

dev_type_open(hdaudioopen);
dev_type_close(hdaudioclose);
dev_type_ioctl(hdaudioioctl);

const struct cdevsw hdaudio_cdevsw = {
        .d_open = hdaudioopen,
        .d_close = hdaudioclose,
        .d_read = noread,
        .d_write = nowrite,
        .d_ioctl = hdaudioioctl,
        .d_stop = nostop,
        .d_tty = notty,
        .d_poll = nopoll,
        .d_mmap = nommap,
        .d_kqfilter = nokqfilter,
        .d_discard = nodiscard,
        .d_flag = D_OTHER
};

extern struct cfdriver hdaudio_cd;

#define HDAUDIOUNIT(x)  minor((x))

static void
hdaudio_stream_init(struct hdaudio_softc *sc, int nis, int nos, int nbidir)
{
        int i, cnt = 0;

        for (i = 0; i < nis && cnt < HDAUDIO_MAX_STREAMS; i++) {
                sc->sc_stream[cnt].st_host = sc;
                sc->sc_stream[cnt].st_enable = true;
                sc->sc_stream[cnt].st_shift = cnt;
                sc->sc_stream[cnt++].st_type = HDAUDIO_STREAM_ISS;
        }
        for (i = 0; i < nos && cnt < HDAUDIO_MAX_STREAMS; i++) {
                sc->sc_stream[cnt].st_host = sc;
                sc->sc_stream[cnt].st_enable = true;
                sc->sc_stream[cnt].st_shift = cnt;
                sc->sc_stream[cnt++].st_type = HDAUDIO_STREAM_OSS;
        }
        for (i = 0; i < nbidir && cnt < HDAUDIO_MAX_STREAMS; i++) {
                sc->sc_stream[cnt].st_host = sc;
                sc->sc_stream[cnt].st_enable = true;
                sc->sc_stream[cnt].st_shift = cnt;
                sc->sc_stream[cnt++].st_type = HDAUDIO_STREAM_BSS;
        }

        for (i = 0; i < cnt; i++)
                hdaudio_stream_stop(&sc->sc_stream[i]);

        sc->sc_stream_mask = 0;
}

static void
hdaudio_codec_init(struct hdaudio_softc *sc)
{
        int i;

        for (i = 0; i < HDAUDIO_MAX_CODECS; i++) {
                sc->sc_codec[i].co_addr = i;
                sc->sc_codec[i].co_host = sc;
        }
}

static void
hdaudio_init(struct hdaudio_softc *sc)
{
        const uint8_t vmaj = hda_read1(sc, HDAUDIO_MMIO_VMAJ);
        const uint8_t vmin = hda_read1(sc, HDAUDIO_MMIO_VMIN);
        const uint16_t gcap = hda_read2(sc, HDAUDIO_MMIO_GCAP);
        const int nis = HDAUDIO_GCAP_ISS(gcap);
        const int nos = HDAUDIO_GCAP_OSS(gcap);
        const int nbidir = HDAUDIO_GCAP_BSS(gcap);
        const int nsdo = HDAUDIO_GCAP_NSDO(gcap);
        const int addr64 = HDAUDIO_GCAP_64OK(gcap);

        hda_print(sc, "HDA ver. %d.%d, OSS %d, ISS %d, BSS %d, SDO %d%s\n",
            vmaj, vmin, nos, nis, nbidir, nsdo, addr64 ? ", 64-bit" : "");

        /* Initialize codecs and streams */
        hdaudio_codec_init(sc);
        hdaudio_stream_init(sc, nis, nos, nbidir);
}

static int
hdaudio_codec_probe(struct hdaudio_softc *sc)
{
        uint16_t statests;
        int codecid;

        statests = hda_read2(sc, HDAUDIO_MMIO_STATESTS);
        for (codecid = 0; codecid < HDAUDIO_MAX_CODECS; codecid++)
                if (statests & (1 << codecid))
                        sc->sc_codec[codecid].co_valid = true;
        hda_write2(sc, HDAUDIO_MMIO_STATESTS, statests);

        return statests;
}

int
hdaudio_dma_alloc(struct hdaudio_softc *sc, struct hdaudio_dma *dma,
    int flags)
{
        int err;

        KASSERT(dma->dma_size > 0);

        err = bus_dmamem_alloc(sc->sc_dmat, dma->dma_size, 128, 0,
            dma->dma_segs, sizeof(dma->dma_segs) / sizeof(dma->dma_segs[0]),
            &dma->dma_nsegs, BUS_DMA_WAITOK);
        if (err)
                return err;
        err = bus_dmamem_map(sc->sc_dmat, dma->dma_segs, dma->dma_nsegs,
            dma->dma_size, &dma->dma_addr, BUS_DMA_WAITOK | flags);
        if (err)
                goto free;
        err = bus_dmamap_create(sc->sc_dmat, dma->dma_size, dma->dma_nsegs,
            dma->dma_size, 0, BUS_DMA_WAITOK, &dma->dma_map);
        if (err)
                goto unmap;
        err = bus_dmamap_load(sc->sc_dmat, dma->dma_map, dma->dma_addr,
            dma->dma_size, NULL, BUS_DMA_WAITOK | flags);
        if (err)
                goto destroy;

        memset(dma->dma_addr, 0, dma->dma_size);
        bus_dmamap_sync(sc->sc_dmat, dma->dma_map, 0, dma->dma_size,
            BUS_DMASYNC_PREWRITE);

        dma->dma_valid = true;
        return 0;

destroy:
        bus_dmamap_destroy(sc->sc_dmat, dma->dma_map);  
unmap:
        bus_dmamem_unmap(sc->sc_dmat, dma->dma_addr, dma->dma_size);
free:
        bus_dmamem_free(sc->sc_dmat, dma->dma_segs, dma->dma_nsegs);

        dma->dma_valid = false;
        return err;
}

void
hdaudio_dma_free(struct hdaudio_softc *sc, struct hdaudio_dma *dma)
{
        if (dma->dma_valid == false)
                return;
        bus_dmamap_unload(sc->sc_dmat, dma->dma_map);
        bus_dmamap_destroy(sc->sc_dmat, dma->dma_map);  
        bus_dmamem_unmap(sc->sc_dmat, dma->dma_addr, dma->dma_size);
        bus_dmamem_free(sc->sc_dmat, dma->dma_segs, dma->dma_nsegs);
        dma->dma_valid = false;
}

static void
hdaudio_corb_enqueue(struct hdaudio_softc *sc, int addr, int nid,
    uint32_t control, uint32_t param)
{
        uint32_t *corb = DMA_KERNADDR(&sc->sc_corb);
        uint32_t verb;
        uint16_t corbrp;
        int wp;

        /* Build command */
        verb = (addr << 28) | (nid << 20) | (control << 8) | param;

        /* Fetch and update write pointer */
        corbrp = hda_read2(sc, HDAUDIO_MMIO_CORBWP);
        wp = (corbrp & 0xff) + 1;
        if (wp >= (sc->sc_corb.dma_size / sizeof(*corb)))
                wp = 0;

        /* Enqueue command */
        bus_dmamap_sync(sc->sc_dmat, sc->sc_corb.dma_map, 0,
            sc->sc_corb.dma_size, BUS_DMASYNC_POSTWRITE);
        corb[wp] = verb;
        bus_dmamap_sync(sc->sc_dmat, sc->sc_corb.dma_map, 0,
            sc->sc_corb.dma_size, BUS_DMASYNC_PREWRITE);

        /* Commit updated write pointer */
        hda_write2(sc, HDAUDIO_MMIO_CORBWP, wp);
}

static void
hdaudio_rirb_unsol(struct hdaudio_softc *sc, struct rirb_entry *entry)
{
        struct hdaudio_codec *co;
        struct hdaudio_function_group *fg;
        uint8_t codecid = RIRB_CODEC_ID(entry);
        unsigned int i;

        if (codecid >= HDAUDIO_MAX_CODECS) {
                hda_error(sc, "unsol: codec id 0x%02x out of range\n", codecid);
                return;
        }
        co = &sc->sc_codec[codecid];
        if (sc->sc_codec[codecid].co_valid == false) {
                hda_error(sc, "unsol: codec id 0x%02x not valid\n", codecid);
                return;
        }

        for (i = 0; i < co->co_nfg; i++) {
                fg = &co->co_fg[i];
                if (fg->fg_device && fg->fg_unsol)
                        fg->fg_unsol(fg->fg_device, entry->resp);
        }
}

static uint32_t
hdaudio_rirb_dequeue(struct hdaudio_softc *sc, bool unsol)
{
        uint16_t rirbwp;
        uint64_t *rirb = DMA_KERNADDR(&sc->sc_rirb);
        struct rirb_entry entry;
        int retry;

        for (;;) {
                retry = HDAUDIO_RIRB_TIMEOUT;

                rirbwp = hda_read2(sc, HDAUDIO_MMIO_RIRBWP);
                while (--retry > 0 && (rirbwp & 0xff) == sc->sc_rirbrp) {
                        if (unsol) {
                                /* don't wait for more unsol events */
                                hda_trace(sc, "unsol: rirb empty\n");
                                return 0xffffffff;
                        }
                        hda_delay(10);
                        rirbwp = hda_read2(sc, HDAUDIO_MMIO_RIRBWP);
                }
                if (retry == 0) {
                        hda_error(sc, "RIRB timeout\n");
                        return 0xffffffff;
                }

                sc->sc_rirbrp++;
                if (sc->sc_rirbrp >= (sc->sc_rirb.dma_size / sizeof(*rirb)))
                        sc->sc_rirbrp = 0;

                bus_dmamap_sync(sc->sc_dmat, sc->sc_rirb.dma_map, 0,
                    sc->sc_rirb.dma_size, BUS_DMASYNC_POSTREAD);
                entry = *(struct rirb_entry *)&rirb[sc->sc_rirbrp];
                bus_dmamap_sync(sc->sc_dmat, sc->sc_rirb.dma_map, 0,
                    sc->sc_rirb.dma_size, BUS_DMASYNC_PREREAD);

                hda_trace(sc, "%s: response %08X %08X\n",
                    unsol ? "unsol" : "cmd  ",
                    entry.resp, entry.resp_ex);

                if (RIRB_UNSOL(&entry)) {
                        hdaudio_rirb_unsol(sc, &entry);
                        continue;
                }

                return entry.resp;
        }
}

uint32_t
hdaudio_command(struct hdaudio_codec *co, int nid, uint32_t control,
    uint32_t param)
{
        uint32_t result;
        struct hdaudio_softc *sc = co->co_host;
        mutex_enter(&sc->sc_corb_mtx);
        result = hdaudio_command_unlocked(co, nid, control, param);
        mutex_exit(&sc->sc_corb_mtx);
        return result;
}

uint32_t
hdaudio_command_unlocked(struct hdaudio_codec *co, int nid, uint32_t control,
    uint32_t param)
{
        struct hdaudio_softc *sc = co->co_host;
        uint32_t result;

        hda_trace(sc, "cmd  : request %08X %08X (%02X)\n",
            control, param, nid);
        hdaudio_corb_enqueue(sc, co->co_addr, nid, control, param);
        result = hdaudio_rirb_dequeue(sc, false);

        /* Clear response interrupt status */
        hda_write1(sc, HDAUDIO_MMIO_RIRBSTS, hda_read1(sc, HDAUDIO_MMIO_RIRBSTS));

        return result;
}

static int
hdaudio_corb_setsize(struct hdaudio_softc *sc)
{
        uint8_t corbsize;
        bus_size_t bufsize = 0;

        /*
         * The size of the CORB is programmable to 2, 16, or 256 entries
         * by using the CORBSIZE register. Choose a size based on the
         * controller capabilities, preferring a larger size when possible.
         */
        corbsize = hda_read1(sc, HDAUDIO_MMIO_CORBSIZE);
        corbsize &= ~0x3;
        if ((corbsize >> 4) & 0x4) {
                corbsize |= 0x2;
                bufsize = 1024;
        } else if ((corbsize >> 4) & 0x2) {
                corbsize |= 0x1;
                bufsize = 64;
        } else if ((corbsize >> 4) & 0x1) {
                corbsize |= 0x0;
                bufsize = 8;
        } else {
                hda_error(sc, "couldn't configure CORB size\n");
                return ENXIO;
        }

#if defined(HDAUDIO_DEBUG)
        hda_print(sc, "using %d byte CORB (cap %X)\n",
            (int)bufsize, corbsize >> 4);
#endif

        sc->sc_corb.dma_size = bufsize;
        sc->sc_corb.dma_sizereg = corbsize;

        return 0;
}

static int
hdaudio_corb_config(struct hdaudio_softc *sc)
{
        uint32_t corbubase, corblbase;
        uint16_t corbrp;
        int retry = HDAUDIO_CORB_TIMEOUT;

        /* Program command buffer base address and size */
        corblbase = (uint32_t)DMA_DMAADDR(&sc->sc_corb);
        corbubase = (uint32_t)(((uint64_t)DMA_DMAADDR(&sc->sc_corb)) >> 32);
        hda_write4(sc, HDAUDIO_MMIO_CORBLBASE, corblbase);
        hda_write4(sc, HDAUDIO_MMIO_CORBUBASE, corbubase);
        hda_write1(sc, HDAUDIO_MMIO_CORBSIZE, sc->sc_corb.dma_sizereg);

        /* Clear the read and write pointers */
        hda_write2(sc, HDAUDIO_MMIO_CORBRP, HDAUDIO_CORBRP_RP_RESET);
        hda_write2(sc, HDAUDIO_MMIO_CORBRP, 0);
        do {
                hda_delay(10);
                corbrp = hda_read2(sc, HDAUDIO_MMIO_CORBRP);
        } while (--retry > 0 && (corbrp & HDAUDIO_CORBRP_RP_RESET) != 0);
        if (retry == 0) {
                hda_error(sc, "timeout resetting CORB\n");
                return ETIME;
        }
        hda_write2(sc, HDAUDIO_MMIO_CORBWP, 0);

        return 0;
}

static int
hdaudio_corb_stop(struct hdaudio_softc *sc)
{
        uint8_t corbctl;
        int retry = HDAUDIO_CORB_TIMEOUT;

        /* Stop the CORB if necessary */
        corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
        if (corbctl & HDAUDIO_CORBCTL_RUN) {
                corbctl &= ~HDAUDIO_CORBCTL_RUN;
                hda_write1(sc, HDAUDIO_MMIO_CORBCTL, corbctl);
                do {
                        hda_delay(10);
                        corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
                } while (--retry > 0 && (corbctl & HDAUDIO_CORBCTL_RUN) != 0);
                if (retry == 0) {
                        hda_error(sc, "timeout stopping CORB\n");
                        return ETIME;
                }
        }

        return 0;
}

static int
hdaudio_corb_start(struct hdaudio_softc *sc)
{
        uint8_t corbctl;
        int retry = HDAUDIO_CORB_TIMEOUT;

        /* Start the CORB if necessary */
        corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
        if ((corbctl & HDAUDIO_CORBCTL_RUN) == 0) {
                corbctl |= HDAUDIO_CORBCTL_RUN;
                hda_write1(sc, HDAUDIO_MMIO_CORBCTL, corbctl);
                do {
                        hda_delay(10);
                        corbctl = hda_read1(sc, HDAUDIO_MMIO_CORBCTL);
                } while (--retry > 0 && (corbctl & HDAUDIO_CORBCTL_RUN) == 0);
                if (retry == 0) {
                        hda_error(sc, "timeout starting CORB\n");
                        return ETIME;
                }
        }

        return 0;
}

static int
hdaudio_rirb_stop(struct hdaudio_softc *sc)
{
        uint8_t rirbctl;
        int retry = HDAUDIO_RIRB_TIMEOUT;

        /* Stop the RIRB if necessary */
        rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
        if (rirbctl & (HDAUDIO_RIRBCTL_RUN|HDAUDIO_RIRBCTL_ROI_EN)) {
                rirbctl &= ~HDAUDIO_RIRBCTL_RUN;
                rirbctl &= ~HDAUDIO_RIRBCTL_ROI_EN;
                hda_write1(sc, HDAUDIO_MMIO_RIRBCTL, rirbctl);
                do {
                        hda_delay(10);
                        rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
                } while (--retry > 0 && (rirbctl & HDAUDIO_RIRBCTL_RUN) != 0);
                if (retry == 0) {
                        hda_error(sc, "timeout stopping RIRB\n");
                        return ETIME;
                }
        }

        return 0;
}

static int
hdaudio_rirb_start(struct hdaudio_softc *sc)
{
        uint8_t rirbctl;
        int retry = HDAUDIO_RIRB_TIMEOUT;

        /* Set the RIRB interrupt count */
        hda_write2(sc, HDAUDIO_MMIO_RINTCNT, 1);

        /* Start the RIRB */
        rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
        rirbctl |= HDAUDIO_RIRBCTL_RUN;
        rirbctl |= HDAUDIO_RIRBCTL_INT_EN;
        hda_write1(sc, HDAUDIO_MMIO_RIRBCTL, rirbctl);
        do {
                hda_delay(10);
                rirbctl = hda_read1(sc, HDAUDIO_MMIO_RIRBCTL);
        } while (--retry > 0 && (rirbctl & HDAUDIO_RIRBCTL_RUN) == 0);
        if (retry == 0) {
                hda_error(sc, "timeout starting RIRB\n");
                return ETIME;
        }

        return 0;
}

static int
hdaudio_rirb_setsize(struct hdaudio_softc *sc)
{
        uint8_t rirbsize;
        bus_size_t bufsize = 0;

        /*
         * The size of the RIRB is programmable to 2, 16, or 256 entries
         * by using the RIRBSIZE register. Choose a size based on the
         * controller capabilities, preferring a larger size when possible.
         */
        rirbsize = hda_read1(sc, HDAUDIO_MMIO_RIRBSIZE);
        rirbsize &= ~0x3;
        if ((rirbsize >> 4) & 0x4) {
                rirbsize |= 0x2;
                bufsize = 2048;
        } else if ((rirbsize >> 4) & 0x2) {
                rirbsize |= 0x1;
                bufsize = 128;
        } else if ((rirbsize >> 4) & 0x1) {
                rirbsize |= 0x0;
                bufsize = 16;
        } else {
                hda_error(sc, "couldn't configure RIRB size\n");
                return ENXIO;
        }

#if defined(HDAUDIO_DEBUG)
        hda_print(sc, "using %d byte RIRB (cap %X)\n",
            (int)bufsize, rirbsize >> 4);
#endif

        sc->sc_rirb.dma_size = bufsize;
        sc->sc_rirb.dma_sizereg = rirbsize;

        return 0;
}

static int
hdaudio_rirb_config(struct hdaudio_softc *sc)
{
        uint32_t rirbubase, rirblbase;

        /* Program command buffer base address and size */
        rirblbase = (uint32_t)DMA_DMAADDR(&sc->sc_rirb);
        rirbubase = (uint32_t)(((uint64_t)DMA_DMAADDR(&sc->sc_rirb)) >> 32);
        hda_write4(sc, HDAUDIO_MMIO_RIRBLBASE, rirblbase);
        hda_write4(sc, HDAUDIO_MMIO_RIRBUBASE, rirbubase);
        hda_write1(sc, HDAUDIO_MMIO_RIRBSIZE, sc->sc_rirb.dma_sizereg);

        /* Clear the write pointer */
        hda_write2(sc, HDAUDIO_MMIO_RIRBWP, HDAUDIO_RIRBWP_WP_RESET);
        sc->sc_rirbrp = 0;

        return 0;
}

static int
hdaudio_reset(struct hdaudio_softc *sc)
{
        int retry = HDAUDIO_RESET_TIMEOUT;
        uint32_t gctl;
        int err;

        if ((err = hdaudio_rirb_stop(sc)) != 0) {
                hda_error(sc, "couldn't reset because RIRB is busy\n");
                return err;
        }
        if ((err = hdaudio_corb_stop(sc)) != 0) {
                hda_error(sc, "couldn't reset because CORB is busy\n");
                return err;
        }

        /* Disable wake events */
        hda_write2(sc, HDAUDIO_MMIO_WAKEEN, 0);

        /* Disable interrupts */
        hda_write4(sc, HDAUDIO_MMIO_INTCTL, 0);

        /* Clear state change status register */
        hda_write2(sc, HDAUDIO_MMIO_STATESTS,
            hda_read2(sc, HDAUDIO_MMIO_STATESTS));
        hda_write1(sc, HDAUDIO_MMIO_RIRBSTS,
            hda_read1(sc, HDAUDIO_MMIO_RIRBSTS));

        /* Put the controller into reset state */
        gctl = hda_read4(sc, HDAUDIO_MMIO_GCTL);
        gctl &= ~HDAUDIO_GCTL_CRST;
        hda_write4(sc, HDAUDIO_MMIO_GCTL, gctl);
        do {
                hda_delay(10);
                gctl = hda_read4(sc, HDAUDIO_MMIO_GCTL);
        } while (--retry > 0 && (gctl & HDAUDIO_GCTL_CRST) != 0);
        if (retry == 0) {
                hda_error(sc, "timeout entering reset state\n");
                return ETIME;
        }

        hda_delay(1000);

        /* Now the controller is in reset state, so bring it out */
        retry = HDAUDIO_RESET_TIMEOUT;
        hda_write4(sc, HDAUDIO_MMIO_GCTL, gctl | HDAUDIO_GCTL_CRST);
        do {
                hda_delay(10);
                gctl = hda_read4(sc, HDAUDIO_MMIO_GCTL);
        } while (--retry > 0 && (gctl & HDAUDIO_GCTL_CRST) == 0);
        if (retry == 0) {
                hda_error(sc, "timeout leaving reset state\n");
                return ETIME;
        }

        hda_delay(2000);

        /* Accept unsolicited responses */
        hda_write4(sc, HDAUDIO_MMIO_GCTL, gctl | HDAUDIO_GCTL_UNSOL_EN);

        return 0;
}

static void
hdaudio_intr_enable(struct hdaudio_softc *sc)
{
        hda_write4(sc, HDAUDIO_MMIO_INTSTS,
            hda_read4(sc, HDAUDIO_MMIO_INTSTS));
        hda_write4(sc, HDAUDIO_MMIO_INTCTL,
            HDAUDIO_INTCTL_GIE | HDAUDIO_INTCTL_CIE);
}

static void
hdaudio_intr_disable(struct hdaudio_softc *sc)
{
        hda_write4(sc, HDAUDIO_MMIO_INTCTL, 0);
}

static int
hdaudio_config_print(void *opaque, const char *pnp)
{
        prop_dictionary_t dict = opaque;
        uint8_t fgtype, nid;
        uint16_t vendor, product;
        const char *type = "unknown";

        prop_dictionary_get_uint8(dict, "function-group-type", &fgtype);
        prop_dictionary_get_uint8(dict, "node-id", &nid);
        prop_dictionary_get_uint16(dict, "vendor-id", &vendor);
        prop_dictionary_get_uint16(dict, "product-id", &product);
        if (pnp) {
                if (fgtype == HDAUDIO_GROUP_TYPE_AFG)
                        type = "hdafg";
                else if (fgtype == HDAUDIO_GROUP_TYPE_VSM_FG)
                        type = "hdvsmfg";

                aprint_normal("%s at %s", type, pnp);
        }
        aprint_debug(" vendor 0x%04X product 0x%04X nid 0x%02X",
            vendor, product, nid);

        return UNCONF;
}

static void
hdaudio_attach_fg(struct hdaudio_function_group *fg, prop_array_t config)
{
        struct hdaudio_codec *co = fg->fg_codec;
        struct hdaudio_softc *sc = co->co_host;
        prop_dictionary_t args = prop_dictionary_create();
        uint64_t fgptr = (vaddr_t)fg;
        int locs[1];

        prop_dictionary_set_uint8(args, "function-group-type", fg->fg_type);
        prop_dictionary_set_uint64(args, "function-group", fgptr);
        prop_dictionary_set_uint8(args, "node-id", fg->fg_nid);
        prop_dictionary_set_uint16(args, "vendor-id", fg->fg_vendor);
        prop_dictionary_set_uint16(args, "product-id", fg->fg_product);
        if (config)
                prop_dictionary_set(args, "pin-config", config);

        locs[0] = fg->fg_nid;

        fg->fg_device = config_found(sc->sc_dev, args, hdaudio_config_print,
            CFARGS(.submatch = config_stdsubmatch,
                   .locators = locs));

        prop_object_release(args);
}

static void
hdaudio_codec_attach(struct hdaudio_codec *co)
{
        struct hdaudio_softc *sc = co->co_host;
        struct hdaudio_function_group *fg;
        uint32_t vid, snc, fgrp;
        int starting_node, num_nodes, nid;

        if (co->co_valid == false)
                return;

        vid = hdaudio_command(co, 0, CORB_GET_PARAMETER, COP_VENDOR_ID);
        snc = hdaudio_command(co, 0, CORB_GET_PARAMETER,
            COP_SUBORDINATE_NODE_COUNT);

        /* make sure the vendor and product IDs are valid */
        if (vid == 0xffffffff || vid == 0x00000000)
                return;

#ifdef HDAUDIO_DEBUG
        uint32_t rid = hdaudio_command(co, 0, CORB_GET_PARAMETER,
            COP_REVISION_ID);
        hda_print(sc, "Codec%02X: %04X:%04X HDA %d.%d rev %d stepping %d\n",
            co->co_addr, vid >> 16, vid & 0xffff,
            (rid >> 20) & 0xf, (rid >> 16) & 0xf,
            (rid >> 8) & 0xff, rid & 0xff);
#endif
        starting_node = (snc >> 16) & 0xff;
        num_nodes = snc & 0xff;

        /*
         * If the total number of nodes is 0, there's nothing we can do.
         * This shouldn't happen, so complain about it.
         */
        if (num_nodes == 0) {
                hda_error(sc, "Codec%02X: No subordinate nodes found (%08x)\n",
                    co->co_addr, snc);
                return;
        }

        co->co_nfg = num_nodes;
        co->co_fg = kmem_zalloc(co->co_nfg * sizeof(*co->co_fg), KM_SLEEP);

        for (nid = starting_node; nid < starting_node + num_nodes; nid++) {
                fg = &co->co_fg[nid - starting_node];
                fg->fg_codec = co;
                fg->fg_nid = nid;
                fg->fg_vendor = vid >> 16;
                fg->fg_product = vid & 0xffff;

                fgrp = hdaudio_command(co, nid, CORB_GET_PARAMETER,
                    COP_FUNCTION_GROUP_TYPE);
                switch (fgrp & 0xff) {
                case 0x01:      /* Audio Function Group */
                        fg->fg_type = HDAUDIO_GROUP_TYPE_AFG;
                        break;
                case 0x02:      /* Vendor Specific Modem Function Group */
                        fg->fg_type = HDAUDIO_GROUP_TYPE_VSM_FG;
                        break;
                default:
                        /* Function group type not supported */
                        fg->fg_type = HDAUDIO_GROUP_TYPE_UNKNOWN;
                        break;
                }
                hdaudio_attach_fg(fg, NULL);
        }
}

int
hdaudio_stream_tag(struct hdaudio_stream *st)
{
        int ret = 0;

        switch (st->st_type) {
        case HDAUDIO_STREAM_ISS:
                ret = 1;
                break;
        case HDAUDIO_STREAM_OSS:
                ret = 2;
                break;
        case HDAUDIO_STREAM_BSS:
                ret = 3;
                break;
        }

        return ret;
}

int
hdaudio_attach(device_t dev, struct hdaudio_softc *sc)
{
        int err, i;

        KASSERT(sc->sc_memvalid == true);

        sc->sc_dev = dev;
        mutex_init(&sc->sc_corb_mtx, MUTEX_DEFAULT, IPL_AUDIO);
        mutex_init(&sc->sc_stream_mtx, MUTEX_DEFAULT, IPL_AUDIO);

        /*
         * Put the controller into a known state by entering and leaving
         * CRST as necessary.
         */
        if ((err = hdaudio_reset(sc)) != 0)
                goto fail;

        /*
         * From the spec:
         *
         * Must wait 250us after reading CRST as a 1 before assuming that
         * codecs have all made status change requests and have been
         * registered by the controller.
         *
         * In reality, we need to wait longer than this.
         */
        hda_delay(HDAUDIO_CODEC_DELAY);

        /*
         * Read device capabilities
         */
        hdaudio_init(sc);

        /*
         * Detect codecs
         */
        if (hdaudio_codec_probe(sc) == 0) {
                hda_error(sc, "no codecs found\n");
                err = ENODEV;
                goto fail;
        }

        /*
         * Ensure that the device is in a known state
         */
        hda_write2(sc, HDAUDIO_MMIO_STATESTS, HDAUDIO_STATESTS_SDIWAKE);
        hda_write1(sc, HDAUDIO_MMIO_RIRBSTS,
            HDAUDIO_RIRBSTS_RIRBOIS | HDAUDIO_RIRBSTS_RINTFL);
        hda_write4(sc, HDAUDIO_MMIO_INTSTS,
            hda_read4(sc, HDAUDIO_MMIO_INTSTS));
        hda_write4(sc, HDAUDIO_MMIO_DPLBASE, 0);
        hda_write4(sc, HDAUDIO_MMIO_DPUBASE, 0);

        /*
         * Initialize the CORB. First negotiate a command buffer size,
         * then allocate and configure it.
         */
        if ((err = hdaudio_corb_setsize(sc)) != 0)
                goto fail;
        if ((err = hdaudio_dma_alloc(sc, &sc->sc_corb, BUS_DMA_WRITE)) != 0)
                goto fail;
        if ((err = hdaudio_corb_config(sc)) != 0)
                goto fail;

        /*
         * Initialize the RIRB.
         */
        if ((err = hdaudio_rirb_setsize(sc)) != 0)
                goto fail;
        if ((err = hdaudio_dma_alloc(sc, &sc->sc_rirb, BUS_DMA_READ)) != 0)
                goto fail;
        if ((err = hdaudio_rirb_config(sc)) != 0)
                goto fail;

        /*
         * Start the CORB and RIRB
         */
        if ((err = hdaudio_corb_start(sc)) != 0)
                goto fail;
        if ((err = hdaudio_rirb_start(sc)) != 0)
                goto fail;

        /*
         * Identify and attach discovered codecs
         */
        for (i = 0; i < HDAUDIO_MAX_CODECS; i++)
                hdaudio_codec_attach(&sc->sc_codec[i]);

        /*
         * Enable interrupts
         */
        hdaudio_intr_enable(sc);

fail:
        if (err)
                hda_error(sc, "device driver failed to attach\n");
        return err;
}

int
hdaudio_detach(struct hdaudio_softc *sc, int flags)
{
        int error;

        /* Disable interrupts */
        hdaudio_intr_disable(sc);

        error = config_detach_children(sc->sc_dev, flags);
        if (error != 0) {
                hdaudio_intr_enable(sc);
                return error;
        }

        mutex_destroy(&sc->sc_corb_mtx);
        mutex_destroy(&sc->sc_stream_mtx);

        hdaudio_dma_free(sc, &sc->sc_corb);
        hdaudio_dma_free(sc, &sc->sc_rirb);

        return 0;
}

bool
hdaudio_resume(struct hdaudio_softc *sc)
{
        if (hdaudio_reset(sc) != 0)
                return false;

        hda_delay(HDAUDIO_CODEC_DELAY);

        /*
         * Ensure that the device is in a known state
         */
        hda_write2(sc, HDAUDIO_MMIO_STATESTS, HDAUDIO_STATESTS_SDIWAKE);
        hda_write1(sc, HDAUDIO_MMIO_RIRBSTS,
            HDAUDIO_RIRBSTS_RIRBOIS | HDAUDIO_RIRBSTS_RINTFL);
        hda_write4(sc, HDAUDIO_MMIO_INTSTS,
            hda_read4(sc, HDAUDIO_MMIO_INTSTS));
        hda_write4(sc, HDAUDIO_MMIO_DPLBASE, 0);
        hda_write4(sc, HDAUDIO_MMIO_DPUBASE, 0);

        if (hdaudio_corb_config(sc) != 0)
                return false;
        if (hdaudio_rirb_config(sc) != 0)
                return false;
        if (hdaudio_corb_start(sc) != 0)
                return false;
        if (hdaudio_rirb_start(sc) != 0)
                return false;

        hdaudio_intr_enable(sc);

        return true;
}

int
hdaudio_rescan(struct hdaudio_softc *sc, const char *ifattr, const int *locs)
{
        struct hdaudio_codec *co;
        struct hdaudio_function_group *fg;
        unsigned int codec;

        for (codec = 0; codec < HDAUDIO_MAX_CODECS; codec++) {
                co = &sc->sc_codec[codec];
                fg = co->co_fg;
                if (!co->co_valid || fg == NULL)
                        continue;
                if (fg->fg_device)
                        continue;
                hdaudio_attach_fg(fg, NULL);
        }

        return 0;
}

void
hdaudio_childdet(struct hdaudio_softc *sc, device_t child)
{
        struct hdaudio_codec *co;
        struct hdaudio_function_group *fg;
        unsigned int codec;

        for (codec = 0; codec < HDAUDIO_MAX_CODECS; codec++) {
                co = &sc->sc_codec[codec];
                fg = co->co_fg;
                if (!co->co_valid || fg == NULL)
                        continue;
                if (fg->fg_device == child)
                        fg->fg_device = NULL;
        }
}

int
hdaudio_intr(struct hdaudio_softc *sc)
{
        struct hdaudio_stream *st;
        uint32_t intsts, stream_mask;
        int streamid = 0;
        uint8_t rirbsts;

        intsts = hda_read4(sc, HDAUDIO_MMIO_INTSTS);
        if (!(intsts & HDAUDIO_INTSTS_GIS))
                return 0;

        if (intsts & HDAUDIO_INTSTS_CIS) {
                rirbsts = hda_read1(sc, HDAUDIO_MMIO_RIRBSTS);
                if (rirbsts & HDAUDIO_RIRBSTS_RINTFL) {
                        mutex_enter(&sc->sc_corb_mtx);
                        hdaudio_rirb_dequeue(sc, true);
                        mutex_exit(&sc->sc_corb_mtx);
                }
                if (rirbsts & (HDAUDIO_RIRBSTS_RIRBOIS|HDAUDIO_RIRBSTS_RINTFL))
                        hda_write1(sc, HDAUDIO_MMIO_RIRBSTS, rirbsts);
                hda_write4(sc, HDAUDIO_MMIO_INTSTS, HDAUDIO_INTSTS_CIS);
        }
        if (intsts & HDAUDIO_INTSTS_SIS_MASK) {
                mutex_enter(&sc->sc_stream_mtx);
                stream_mask = intsts & sc->sc_stream_mask;
                while (streamid < HDAUDIO_MAX_STREAMS && stream_mask != 0) {
                        st = &sc->sc_stream[streamid++];
                        if ((stream_mask & 1) != 0 && st->st_intr) {
                                st->st_intr(st);
                        }
                        stream_mask >>= 1;
                }
                mutex_exit(&sc->sc_stream_mtx);
                hda_write4(sc, HDAUDIO_MMIO_INTSTS, HDAUDIO_INTSTS_SIS_MASK);
        }

        return 1;
}

struct hdaudio_stream *
hdaudio_stream_establish(struct hdaudio_softc *sc,
    enum hdaudio_stream_type type, int (*intr)(struct hdaudio_stream *),
    void *cookie)
{
        struct hdaudio_stream *st;
        struct hdaudio_dma dma;
        int i, err;

        dma.dma_size = sizeof(struct hdaudio_bdl_entry) * HDAUDIO_BDL_MAX;
        dma.dma_sizereg = 0;
        err = hdaudio_dma_alloc(sc, &dma, BUS_DMA_COHERENT | BUS_DMA_NOCACHE);
        if (err)
                return NULL;

        mutex_enter(&sc->sc_stream_mtx);
        for (i = 0; i < HDAUDIO_MAX_STREAMS; i++) {
                st = &sc->sc_stream[i];
                if (st->st_enable == false)
                        break;
                if (st->st_type != type)
                        continue;
                if (sc->sc_stream_mask & (1 << i))
                        continue;

                /* Allocate stream */
                st->st_bdl = dma;
                st->st_intr = intr;
                st->st_cookie = cookie;
                sc->sc_stream_mask |= (1 << i);
                mutex_exit(&sc->sc_stream_mtx);
                return st;
        }
        mutex_exit(&sc->sc_stream_mtx);

        /* No streams of requested type available */
        hdaudio_dma_free(sc, &dma);
        return NULL;
}

void
hdaudio_stream_disestablish(struct hdaudio_stream *st)
{
        struct hdaudio_softc *sc = st->st_host;
        struct hdaudio_dma dma;

        KASSERT(sc->sc_stream_mask & (1 << st->st_shift));

        mutex_enter(&sc->sc_stream_mtx);
        sc->sc_stream_mask &= ~(1 << st->st_shift);
        st->st_intr = NULL;
        st->st_cookie = NULL;
        dma = st->st_bdl;
        st->st_bdl.dma_valid = false;
        mutex_exit(&sc->sc_stream_mtx);

        /* Can't bus_dmamem_unmap while holding a mutex.  */
        hdaudio_dma_free(sc, &dma);
}

/*
 * Convert most of audio_params_t to stream fmt descriptor; noticeably missing
 * is the # channels bits, as this is encoded differently in codec and
 * stream descriptors.
 *
 * TODO: validate that the stream and selected codecs can handle the fmt
 */
uint16_t
hdaudio_stream_param(struct hdaudio_stream *st, const audio_params_t *param)
{
        uint16_t fmt = 0;

        switch (param->encoding) {
        case AUDIO_ENCODING_AC3:
                fmt |= HDAUDIO_FMT_TYPE_NONPCM;
                break;
        default:
                fmt |= HDAUDIO_FMT_TYPE_PCM;
                break;
        }

        switch (param->sample_rate) {
        case 8000:
                fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(1) |
                    HDAUDIO_FMT_DIV(6);
                break;
        case 11025:
                fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(1) |
                    HDAUDIO_FMT_DIV(4);
                break;
        case 16000:
                fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(1) |
                    HDAUDIO_FMT_DIV(3);
                break;
        case 22050:
                fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(1) |
                    HDAUDIO_FMT_DIV(2);
                break;
        case 32000:
                fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(2) |
                    HDAUDIO_FMT_DIV(3);
                break;
        case 44100:
                fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(1);
                break;
        case 48000:
                fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(1);
                break;
        case 88200:
                fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(2);
                break;
        case 96000:
                fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(2);
                break;
        case 176400:
                fmt |= HDAUDIO_FMT_BASE_44 | HDAUDIO_FMT_MULT(4);
                break;
        case 192000:
                fmt |= HDAUDIO_FMT_BASE_48 | HDAUDIO_FMT_MULT(4);
                break;
        default:
                return 0;
        }

        if (param->precision == 16 && param->validbits == 8)
                fmt |= HDAUDIO_FMT_BITS_8_16;
        else if (param->precision == 16 && param->validbits == 16)
                fmt |= HDAUDIO_FMT_BITS_16_16;
        else if (param->precision == 32 && param->validbits == 20)
                fmt |= HDAUDIO_FMT_BITS_20_32;
        else if (param->precision == 32 && param->validbits == 24)
                fmt |= HDAUDIO_FMT_BITS_24_32;
        else if (param->precision == 32 && param->validbits == 32)
                fmt |= HDAUDIO_FMT_BITS_32_32;
        else
                return 0;

        return fmt;
}

void
hdaudio_stream_reset(struct hdaudio_stream *st)
{
        struct hdaudio_softc *sc = st->st_host;
        int snum = st->st_shift;
        int retry;
        uint8_t ctl0;

        ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
        ctl0 |= HDAUDIO_CTL_SRST;
        hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);

        retry = HDAUDIO_RESET_TIMEOUT;
        do {
                ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
                if (ctl0 & HDAUDIO_CTL_SRST)
                        break;
                hda_delay(10);
        } while (--retry > 0);

        ctl0 &= ~HDAUDIO_CTL_SRST;
        hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);

        retry = HDAUDIO_RESET_TIMEOUT;
        do {
                ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
                if (!(ctl0 & HDAUDIO_CTL_SRST))
                        break;
                hda_delay(10);
        } while (--retry > 0);
        if (retry == 0) {
                hda_error(sc, "timeout leaving stream reset state\n");
                return;
        }
}

void
hdaudio_stream_start(struct hdaudio_stream *st, int blksize,
    bus_size_t dmasize, const audio_params_t *params)
{
        struct hdaudio_softc *sc = st->st_host;
        struct hdaudio_bdl_entry *bdl;
        uint64_t dmaaddr;
        uint32_t intctl;
        uint16_t fmt;
        uint8_t ctl0, ctl2;
        int cnt, snum = st->st_shift;

        KASSERT(sc->sc_stream_mask & (1 << st->st_shift));
        KASSERT(st->st_data.dma_valid == true);
        KASSERT(st->st_bdl.dma_valid == true);

        hdaudio_stream_stop(st);
        hdaudio_stream_reset(st);

        /*
         * Configure buffer descriptor list
         */
        dmaaddr = DMA_DMAADDR(&st->st_data);
        bdl = DMA_KERNADDR(&st->st_bdl);
        for (cnt = 0; cnt < HDAUDIO_BDL_MAX; cnt++) {
                bdl[cnt].address_lo = (uint32_t)dmaaddr;
                bdl[cnt].address_hi = dmaaddr >> 32;
                bdl[cnt].length = blksize;
                bdl[cnt].flags = HDAUDIO_BDL_ENTRY_IOC;
                dmaaddr += blksize;
                if (dmaaddr >= DMA_DMAADDR(&st->st_data) + dmasize) {
                        cnt++;
                        break;
                }
        }

        /*
         * Program buffer descriptor list
         */
        dmaaddr = DMA_DMAADDR(&st->st_bdl);
        hda_write4(sc, HDAUDIO_SD_BDPL(snum), (uint32_t)dmaaddr);
        hda_write4(sc, HDAUDIO_SD_BDPU(snum), (uint32_t)(dmaaddr >> 32));
        hda_write2(sc, HDAUDIO_SD_LVI(snum), (cnt - 1) & 0xff);

        /*
         * Program cyclic buffer length
         */
        hda_write4(sc, HDAUDIO_SD_CBL(snum), dmasize);

        /*
         * Program stream number (tag). Although controller hardware is
         * capable of transmitting any stream number (0-15), by convention
         * stream 0 is reserved as unused by software, so that converters
         * whose stream numbers have been reset to 0 do not unintentionally
         * decode data not intended for them.
         */
        ctl2 = hda_read1(sc, HDAUDIO_SD_CTL2(snum));
        ctl2 &= ~0xf0;
        ctl2 |= hdaudio_stream_tag(st) << 4;
        hda_write1(sc, HDAUDIO_SD_CTL2(snum), ctl2);

        /*
         * Program stream format
         */
        fmt = hdaudio_stream_param(st, params) |
            HDAUDIO_FMT_CHAN(params->channels);
        hda_write2(sc, HDAUDIO_SD_FMT(snum), fmt);

        /*
         * Switch on interrupts for this stream
         */
        intctl = hda_read4(sc, HDAUDIO_MMIO_INTCTL);
        intctl |= (1 << st->st_shift);
        hda_write4(sc, HDAUDIO_MMIO_INTCTL, intctl);

        /*
         * Start running the stream
         */
        ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
        ctl0 |= HDAUDIO_CTL_DEIE | HDAUDIO_CTL_FEIE | HDAUDIO_CTL_IOCE |
            HDAUDIO_CTL_RUN;
        hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);
}

void
hdaudio_stream_stop(struct hdaudio_stream *st)
{
        struct hdaudio_softc *sc = st->st_host;
        uint32_t intctl;
        uint8_t ctl0;
        int snum = st->st_shift;

        /*
         * Stop running the stream
         */
        ctl0 = hda_read1(sc, HDAUDIO_SD_CTL0(snum));
        ctl0 &= ~(HDAUDIO_CTL_DEIE | HDAUDIO_CTL_FEIE | HDAUDIO_CTL_IOCE |
            HDAUDIO_CTL_RUN);
        hda_write1(sc, HDAUDIO_SD_CTL0(snum), ctl0);

        /*
         * Switch off interrupts for this stream
         */
        intctl = hda_read4(sc, HDAUDIO_MMIO_INTCTL);
        intctl &= ~(1 << st->st_shift);
        hda_write4(sc, HDAUDIO_MMIO_INTCTL, intctl);
}

/*
 * /dev/hdaudioN interface
 */

static const char *
hdaudioioctl_fgrp_to_cstr(enum function_group_type type)
{
        switch (type) {
        case HDAUDIO_GROUP_TYPE_AFG:
                return "afg";
        case HDAUDIO_GROUP_TYPE_VSM_FG:
                return "vsmfg";
        default:
                return "unknown";
        }
}

static struct hdaudio_function_group *
hdaudioioctl_fgrp_lookup(struct hdaudio_softc *sc, int codecid, int nid)
{
        struct hdaudio_codec *co;
        struct hdaudio_function_group *fg = NULL;
        int i;

        if (codecid < 0 || codecid >= HDAUDIO_MAX_CODECS)
                return NULL;
        co = &sc->sc_codec[codecid];
        if (co->co_valid == false)
                return NULL;

        for (i = 0; i < co->co_nfg; i++)
                if (co->co_fg[i].fg_nid == nid) {
                        fg = &co->co_fg[i];
                        break;
                }

        return fg;
}

static int
hdaudioioctl_fgrp_info(struct hdaudio_softc *sc, prop_dictionary_t request,
    prop_dictionary_t response)
{
        struct hdaudio_codec *co;
        struct hdaudio_function_group *fg;
        prop_array_t array;
        prop_dictionary_t dict;
        int codecid, fgid;

        array = prop_array_create();
        if (array == NULL)
                return ENOMEM;

        for (codecid = 0; codecid < HDAUDIO_MAX_CODECS; codecid++) {
                co = &sc->sc_codec[codecid];
                if (co->co_valid == false)
                        continue;
                for (fgid = 0; fgid < co->co_nfg; fgid++) {
                        fg = &co->co_fg[fgid];
                        dict = prop_dictionary_create();
                        if (dict == NULL)
                                return ENOMEM;
                        prop_dictionary_set_string_nocopy(dict,
                            "type", hdaudioioctl_fgrp_to_cstr(fg->fg_type));
                        prop_dictionary_set_int16(dict, "nid", fg->fg_nid);
                        prop_dictionary_set_int16(dict, "codecid", codecid);
                        prop_dictionary_set_uint16(dict, "vendor-id",
                            fg->fg_vendor);
                        prop_dictionary_set_uint16(dict, "product-id",
                            fg->fg_product);
                        prop_dictionary_set_uint32(dict, "subsystem-id",
                            sc->sc_subsystem);
                        if (fg->fg_device)
                                prop_dictionary_set_string(dict, "device",
                                    device_xname(fg->fg_device));
                        else
                                prop_dictionary_set_string_nocopy(dict,
                                    "device", "<none>");
                        prop_array_add(array, dict);
                }
        }

        prop_dictionary_set(response, "function-group-info", array);
        return 0;
}

static int
hdaudioioctl_fgrp_getconfig(struct hdaudio_softc *sc,
    prop_dictionary_t request, prop_dictionary_t response)
{
        struct hdaudio_function_group *fg;
        prop_dictionary_t dict;
        prop_array_t array;
        uint32_t nodecnt, wcap, config;
        int16_t codecid, nid, i;
        int startnode, endnode;

        if (!prop_dictionary_get_int16(request, "codecid", &codecid) ||
            !prop_dictionary_get_int16(request, "nid", &nid))
                return EINVAL;

        fg = hdaudioioctl_fgrp_lookup(sc, codecid, nid);
        if (fg == NULL)
                return ENODEV;

        array = prop_array_create();
        if (array == NULL)
                return ENOMEM;

        nodecnt = hdaudio_command(fg->fg_codec, fg->fg_nid,
            CORB_GET_PARAMETER, COP_SUBORDINATE_NODE_COUNT);
        startnode = COP_NODECNT_STARTNODE(nodecnt);
        endnode = startnode + COP_NODECNT_NUMNODES(nodecnt);

        for (i = startnode; i < endnode; i++) {
                wcap = hdaudio_command(fg->fg_codec, i,
                    CORB_GET_PARAMETER, COP_AUDIO_WIDGET_CAPABILITIES);
                if (COP_AWCAP_TYPE(wcap) != COP_AWCAP_TYPE_PIN_COMPLEX)
                        continue;
                config = hdaudio_command(fg->fg_codec, i,
                    CORB_GET_CONFIGURATION_DEFAULT, 0);
                dict = prop_dictionary_create();
                if (dict == NULL)
                        return ENOMEM;
                prop_dictionary_set_int16(dict, "nid", i);
                prop_dictionary_set_uint32(dict, "config", config);
                prop_array_add(array, dict);
        }

        prop_dictionary_set(response, "pin-config", array);

        return 0;
}

static int
hdaudioioctl_fgrp_setconfig(struct hdaudio_softc *sc,
    prop_dictionary_t request, prop_dictionary_t response)
{
        struct hdaudio_function_group *fg;
        prop_array_t config;
        int16_t codecid, nid;
        int err;

        if (!prop_dictionary_get_int16(request, "codecid", &codecid) ||
            !prop_dictionary_get_int16(request, "nid", &nid))
                return EINVAL;

        fg = hdaudioioctl_fgrp_lookup(sc, codecid, nid);
        if (fg == NULL)
                return ENODEV;

        if (fg->fg_device) {
                err = config_detach(fg->fg_device, 0);
                if (err)
                        return err;
                fg->fg_device = NULL;
        }

        /* "pin-config" may be NULL, this means "use BIOS configuration" */
        config = prop_dictionary_get(request, "pin-config");
        if (config && prop_object_type(config) != PROP_TYPE_ARRAY) {
                prop_object_release(config);
                return EINVAL;
        }
        hdaudio_attach_fg(fg, config);
        if (config)
                prop_object_release(config);

        return 0;
}

static int
hdaudio_dispatch_fgrp_ioctl(struct hdaudio_softc *sc, u_long cmd,
    prop_dictionary_t request, prop_dictionary_t response)
{
        struct hdaudio_function_group *fg;
        int (*infocb)(void *, prop_dictionary_t, prop_dictionary_t);
        prop_dictionary_t fgrp_dict;
        uint64_t info_fn;
        int16_t codecid, nid;
        void *fgrp_sc; 
        bool rv;
        int err;

        if (!prop_dictionary_get_int16(request, "codecid", &codecid) ||
            !prop_dictionary_get_int16(request, "nid", &nid))
                return EINVAL;

        fg = hdaudioioctl_fgrp_lookup(sc, codecid, nid);
        if (fg == NULL)
                return ENODEV;
        if (fg->fg_device == NULL)
                return ENXIO;
        fgrp_sc = device_private(fg->fg_device);
        fgrp_dict = device_properties(fg->fg_device);

        switch (fg->fg_type) {
        case HDAUDIO_GROUP_TYPE_AFG:
                switch (cmd) {
                case HDAUDIO_FGRP_CODEC_INFO:
                        rv = prop_dictionary_get_uint64(fgrp_dict,
                            "codecinfo-callback", &info_fn);
                        if (!rv)
                                return ENXIO;
                        infocb = (void *)(uintptr_t)info_fn;
                        err = infocb(fgrp_sc, request, response);
                        break;
                case HDAUDIO_FGRP_WIDGET_INFO:
                        rv = prop_dictionary_get_uint64(fgrp_dict,
                            "widgetinfo-callback", &info_fn);
                        if (!rv)
                                return ENXIO;
                        infocb = (void *)(uintptr_t)info_fn;
                        err = infocb(fgrp_sc, request, response);
                        break;
                default:
                        err = EINVAL;
                        break;
                }
                break;

        default:
                err = EINVAL;
                break;
        }
        return err;
}

int
hdaudioopen(dev_t dev, int flag, int mode, struct lwp *l)
{
        device_t self;

        self = device_lookup(&hdaudio_cd, HDAUDIOUNIT(dev));
        if (self == NULL)
                return ENXIO;

        return 0;
}

int
hdaudioclose(dev_t dev, int flag, int mode, struct lwp *l)
{
        return 0;
}

int
hdaudioioctl(dev_t dev, u_long cmd, void *addr, int flag, struct lwp *l)
{
        struct hdaudio_softc *sc;
        struct plistref *pref = addr;
        prop_dictionary_t request, response;
        int err;

        sc = device_lookup_private(&hdaudio_cd, HDAUDIOUNIT(dev));
        if (sc == NULL)
                return ENXIO;

        response = prop_dictionary_create();
        if (response == NULL)
                return ENOMEM;

        err = prop_dictionary_copyin_ioctl(pref, cmd, &request);
        if (err) {
                prop_object_release(response);
                return err;
        }

        switch (cmd) {
        case HDAUDIO_FGRP_INFO:
                err = hdaudioioctl_fgrp_info(sc, request, response);
                break;
        case HDAUDIO_FGRP_GETCONFIG:
                err = hdaudioioctl_fgrp_getconfig(sc, request, response);
                break;
        case HDAUDIO_FGRP_SETCONFIG:
                err = hdaudioioctl_fgrp_setconfig(sc, request, response);
                break;
        case HDAUDIO_FGRP_CODEC_INFO:
        case HDAUDIO_FGRP_WIDGET_INFO:
                err = hdaudio_dispatch_fgrp_ioctl(sc, cmd, request, response);
                break;
        default:
                err = EINVAL;
                break;
        }

        if (!err)
                err = prop_dictionary_copyout_ioctl(pref, cmd, response);

        if (response)
                prop_object_release(response);
        prop_object_release(request);
        return err;
}

MODULE(MODULE_CLASS_DRIVER, hdaudio, "audio");
#ifdef _MODULE
static const struct cfiattrdata hdaudiobuscf_iattrdata = {
        "hdaudiobus", 1, {
                { "nid", "-1", -1 },
        }
};
static const struct cfiattrdata * const hdaudio_attrs[] = {
        &hdaudiobuscf_iattrdata, NULL
};
CFDRIVER_DECL(hdaudio, DV_AUDIODEV, hdaudio_attrs);
#endif

static int
hdaudio_modcmd(modcmd_t cmd, void *opaque)
{
        int error = 0;
#ifdef _MODULE
        int bmaj = -1, cmaj = -1;
#endif

        switch (cmd) {
        case MODULE_CMD_INIT:
#ifdef _MODULE
                error = devsw_attach("hdaudio", NULL, &bmaj,
                    &hdaudio_cdevsw, &cmaj);
                if (error)
                        break;
                error = config_cfdriver_attach(&hdaudio_cd);
                if (error)
                        devsw_detach(NULL, &hdaudio_cdevsw);
#endif
                break;
        case MODULE_CMD_FINI:
#ifdef _MODULE
                error = config_cfdriver_detach(&hdaudio_cd);
                if (error)
                        break;
                devsw_detach(NULL, &hdaudio_cdevsw);
#endif
                break;
        default:
                error = ENOTTY;
                break;
        }
        return error;
}

DEV_VERBOSE_DEFINE(hdaudio);