aboutsummaryrefslogtreecommitdiff
path: root/sys/dev/firewire/fwisound.c
diff options
context:
space:
mode:
Diffstat (limited to 'sys/dev/firewire/fwisound.c')
-rw-r--r--sys/dev/firewire/fwisound.c548
1 files changed, 548 insertions, 0 deletions
diff --git a/sys/dev/firewire/fwisound.c b/sys/dev/firewire/fwisound.c
new file mode 100644
index 000000000000..da04ae9f034b
--- /dev/null
+++ b/sys/dev/firewire/fwisound.c
@@ -0,0 +1,548 @@
+/*
+ * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * fwisound(4) - Apple FireWire audio driver
+ *
+ * Protocol reverse-engineered by Clemens Ladisch (Linux isight_audio).
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/module.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/lock.h>
+#include <sys/mutex.h>
+#include <sys/sysctl.h>
+#include <sys/taskqueue.h>
+#include <sys/mbuf.h>
+
+#include <dev/firewire/firewire.h>
+#include <dev/firewire/firewirereg.h>
+#include <dev/firewire/iec13213.h>
+#include <dev/firewire/fwisound.h>
+#include <dev/firewire/fw_helpers.h>
+
+static MALLOC_DEFINE(M_FWISOUND, "fwisound", "Apple FireWire Audio");
+
+static int debug = 0;
+static int iso_channel = FWISOUND_ISO_CHANNEL;
+SYSCTL_DECL(_hw_firewire);
+static SYSCTL_NODE(_hw_firewire, OID_AUTO, fwisound,
+ CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Apple FireWire Audio");
+SYSCTL_INT(_hw_firewire_fwisound, OID_AUTO, debug, CTLFLAG_RWTUN, &debug, 0,
+ "fwisound debug level");
+SYSCTL_INT(_hw_firewire_fwisound, OID_AUTO, iso_channel, CTLFLAG_RWTUN,
+ &iso_channel, 0, "ISO channel for isochronous receive (default 1)");
+
+#define FWISOUND_DEBUG(lev, fmt, ...) \
+ do { \
+ if (debug >= (lev)) \
+ printf("fwisound: " fmt, ## __VA_ARGS__); \
+ } while (0)
+
+static int fwisound_probe(device_t);
+static int fwisound_attach(device_t);
+static int fwisound_detach(device_t);
+static void fwisound_probe_task(void *, int);
+static void fwisound_start_task(void *, int);
+static void fwisound_stop_task(void *, int);
+static void fwisound_iso_input(struct fw_xferq *);
+
+static int
+fwisound_read_quadlet(struct fwisound_softc *sc, uint32_t offset, uint32_t *val)
+{
+ uint16_t dst;
+ uint8_t spd;
+ int err;
+
+ FWISOUND_LOCK(sc);
+ if (sc->fwdev == NULL) {
+ FWISOUND_UNLOCK(sc);
+ return (ENXIO);
+ }
+ dst = FWLOCALBUS | sc->fwdev->dst;
+ spd = min(sc->fwdev->speed, FWSPD_S400);
+ FWISOUND_UNLOCK(sc);
+
+ err = fw_read_quadlet(sc->fd.fc, M_FWISOUND, dst, spd,
+ sc->cmd_hi, sc->cmd_lo + offset, val);
+ if (err)
+ FWISOUND_DEBUG(1, "read_quadlet: offset=0x%x err=%d\n",
+ offset, err);
+ return (err);
+}
+
+static int
+fwisound_write_quadlet(struct fwisound_softc *sc, uint32_t offset, uint32_t val)
+{
+ uint16_t dst;
+ uint8_t spd;
+ int err;
+
+ FWISOUND_LOCK(sc);
+ if (sc->fwdev == NULL) {
+ FWISOUND_UNLOCK(sc);
+ return (ENXIO);
+ }
+ dst = FWLOCALBUS | sc->fwdev->dst;
+ spd = min(sc->fwdev->speed, FWSPD_S400);
+ FWISOUND_UNLOCK(sc);
+
+ err = fw_write_quadlet(sc->fd.fc, M_FWISOUND, dst, spd,
+ sc->cmd_hi, sc->cmd_lo + offset, val);
+ if (err)
+ FWISOUND_DEBUG(1, "write_quadlet: offset=0x%x err=%d\n",
+ offset, err);
+ return (err);
+}
+
+/*
+ * Extract the audio command base register (key 0x40) from the unit directory.
+ */
+static uint32_t
+fwisound_find_audio_base(struct fw_unit *unit)
+{
+ struct fw_device *fwdev = unit->fwdev;
+ struct csrdirectory *udir;
+ struct csrreg *reg;
+ uint32_t udir_qoff, udir_len, rom_quads;
+ int i;
+
+ rom_quads = CSRROMSIZE / 4;
+ udir_qoff = unit->dir_offset / 4;
+ if (udir_qoff >= rom_quads)
+ return (0);
+
+ udir = (struct csrdirectory *)&fwdev->csrrom[udir_qoff];
+ udir_len = udir->crc_len;
+ if (udir_qoff + 1 + udir_len > rom_quads)
+ return (0);
+ if (!crom_crc_valid((uint32_t *)&udir->entry[0], udir_len, udir->crc)) {
+ if (firewire_debug)
+ printf("fwisound: bad CRC in unit directory at 0x%x\n",
+ udir_qoff);
+ }
+
+ for (i = 0; i < (int)udir_len; i++) {
+ if (udir_qoff + 1 + i >= rom_quads)
+ break;
+ reg = &udir->entry[i];
+ if (reg->key == IIDC_CROM_CMD_BASE && reg->val != 0)
+ return (reg->val);
+ }
+ return (0);
+}
+
+static void
+fwisound_probe_task(void *arg, int pending __unused)
+{
+ struct fwisound_softc *sc = (struct fwisound_softc *)arg;
+ uint32_t val;
+ int err;
+
+ if (sc->state == FWISOUND_STATE_DETACHING || sc->fwdev == NULL)
+ return;
+
+ err = fwisound_write_quadlet(sc, FWISOUND_REG_SAMPLE_RATE,
+ FWISOUND_RATE_48000);
+ if (err) {
+ device_printf(sc->dev,
+ "failed to set sample rate: %d\n", err);
+ return;
+ }
+
+ if (fwisound_read_quadlet(sc, FWISOUND_REG_GAIN_RAW_START, &val) == 0)
+ sc->gain_raw_min = (int32_t)val;
+ if (fwisound_read_quadlet(sc, FWISOUND_REG_GAIN_RAW_END, &val) == 0)
+ sc->gain_raw_max = (int32_t)val;
+
+ FWISOUND_LOCK(sc);
+ if (sc->state == FWISOUND_STATE_DETACHING) {
+ FWISOUND_UNLOCK(sc);
+ return;
+ }
+ sc->state = FWISOUND_STATE_PROBED;
+ FWISOUND_UNLOCK(sc);
+}
+
+static void
+fwisound_start_task(void *arg, int pending __unused)
+{
+ struct fwisound_softc *sc = (struct fwisound_softc *)arg;
+
+ if (sc->state == FWISOUND_STATE_DETACHING)
+ return;
+
+ fwisound_iso_start(sc);
+}
+
+static void
+fwisound_stop_task(void *arg, int pending __unused)
+{
+ struct fwisound_softc *sc = (struct fwisound_softc *)arg;
+
+ fwisound_iso_stop(sc);
+
+ FWISOUND_LOCK(sc);
+ if (sc->state != FWISOUND_STATE_DETACHING)
+ sc->state = FWISOUND_STATE_PROBED;
+ FWISOUND_UNLOCK(sc);
+}
+
+int
+fwisound_iso_start(struct fwisound_softc *sc)
+{
+ struct firewire_comm *fc = sc->fd.fc;
+ struct fw_xferq *xferq;
+ uint32_t val;
+ uint8_t speed;
+ int dma_ch, err;
+
+ mtx_assert(&sc->mtx, MA_NOTOWNED);
+
+ FWISOUND_LOCK(sc);
+ if (sc->dma_ch >= 0 || sc->state == FWISOUND_STATE_STREAMING) {
+ FWISOUND_UNLOCK(sc);
+ return (0);
+ }
+ if (sc->state == FWISOUND_STATE_DETACHING || sc->fwdev == NULL) {
+ FWISOUND_UNLOCK(sc);
+ return (ENXIO);
+ }
+ speed = min(sc->fwdev->speed, FWSPD_S400);
+ FWISOUND_UNLOCK(sc);
+
+ dma_ch = fw_open_isodma(fc, 0);
+ if (dma_ch < 0) {
+ device_printf(sc->dev, "no IR DMA channel available\n");
+ return (EBUSY);
+ }
+
+ FWISOUND_LOCK(sc);
+ if (sc->dma_ch >= 0) {
+ FWISOUND_UNLOCK(sc);
+ fc->ir[dma_ch]->flag &= ~FWXFERQ_OPEN;
+ return (0);
+ }
+ FWISOUND_UNLOCK(sc);
+
+ xferq = fc->ir[dma_ch];
+ xferq->flag |= FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_STREAM;
+
+ xferq->flag &= ~FWXFERQ_CHTAGMASK;
+ xferq->flag |= sc->iso_channel & FWXFERQ_CHTAGMASK;
+
+ xferq->sc = (caddr_t)sc;
+ xferq->hand = fwisound_iso_input;
+ xferq->bnchunk = FWISOUND_ISO_NCHUNK;
+ xferq->bnpacket = 1;
+ xferq->psize = FWISOUND_ISO_PKTSIZE;
+ xferq->queued = 0;
+ xferq->buf = NULL;
+
+ xferq->bulkxfer = malloc(
+ sizeof(struct fw_bulkxfer) * xferq->bnchunk,
+ M_FWISOUND, M_WAITOK | M_ZERO);
+
+ fw_iso_init_chunks(xferq);
+
+ sc->sample_total = 0;
+
+ val = sc->iso_channel | ((uint32_t)speed << 16);
+ err = fwisound_write_quadlet(sc, FWISOUND_REG_ISO_TX_CONFIG, val);
+ if (err) {
+ device_printf(sc->dev,
+ "failed to set ISO_TX_CONFIG: %d\n", err);
+ goto fail;
+ }
+
+ err = fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE,
+ FWISOUND_AUDIO_ENABLE);
+ if (err) {
+ device_printf(sc->dev,
+ "failed to enable audio: %d\n", err);
+ goto fail;
+ }
+
+ err = fc->irx_enable(fc, dma_ch);
+ if (err) {
+ device_printf(sc->dev, "failed to start IR DMA: %d\n", err);
+ fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0);
+ goto fail;
+ }
+
+ FWISOUND_LOCK(sc);
+ if (sc->state == FWISOUND_STATE_DETACHING) {
+ FWISOUND_UNLOCK(sc);
+ fc->irx_disable(fc, dma_ch);
+ fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0);
+ err = ENXIO;
+ goto fail;
+ }
+ sc->dma_ch = dma_ch;
+ sc->state = FWISOUND_STATE_STREAMING;
+ FWISOUND_UNLOCK(sc);
+
+ return (0);
+
+fail:
+ fw_iso_free_chunks(xferq, M_FWISOUND);
+ xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_OPEN | FWXFERQ_STREAM |
+ FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK);
+ xferq->hand = NULL;
+ sc->dma_ch = -1;
+ return (err);
+}
+
+void
+fwisound_iso_stop(struct fwisound_softc *sc)
+{
+ struct firewire_comm *fc = sc->fd.fc;
+ struct fw_xferq *xferq;
+ int dma_ch;
+
+ FWISOUND_LOCK(sc);
+ dma_ch = sc->dma_ch;
+ if (dma_ch < 0) {
+ FWISOUND_UNLOCK(sc);
+ return;
+ }
+ sc->dma_ch = -1;
+ FWISOUND_UNLOCK(sc);
+
+ xferq = fc->ir[dma_ch];
+
+ xferq->flag &= ~FWXFERQ_OPEN;
+ xferq->hand = NULL;
+
+ if (xferq->flag & FWXFERQ_RUNNING)
+ fc->irx_disable(fc, dma_ch);
+
+ if (sc->fwdev != NULL)
+ fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0);
+
+ FWISOUND_LOCK(sc);
+ fw_iso_wait_inactive_locked(&sc->mtx, &sc->iso_active, "fwisis");
+ FWISOUND_UNLOCK(sc);
+
+ xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_STREAM |
+ FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK);
+
+ fw_iso_free_chunks(xferq, M_FWISOUND);
+}
+
+static void
+fwisound_iso_input(struct fw_xferq *xferq)
+{
+ struct fwisound_softc *sc = (struct fwisound_softc *)xferq->sc;
+ struct fwisound_chan *chan;
+ struct fw_bulkxfer *sxfer;
+ struct fw_pkt *fp;
+ struct fwisound_payload *pay;
+ struct mbuf *m;
+ uint8_t *ring;
+ uint32_t plen, nbytes, sample_count, ring_size, pos, chunk, old_ptr;
+ int dma_ch, need_intr;
+
+ FWISOUND_LOCK(sc);
+ dma_ch = sc->dma_ch;
+ if (dma_ch < 0) {
+ FWISOUND_UNLOCK(sc);
+ return;
+ }
+ sc->iso_active = 1;
+ chan = sc->chan;
+ FWISOUND_UNLOCK(sc);
+
+ while ((sxfer = STAILQ_FIRST(&xferq->stvalid)) != NULL) {
+ STAILQ_REMOVE_HEAD(&xferq->stvalid, link);
+
+ m = fw_iso_dequeue(xferq, sxfer, sc->fd.fc);
+ if (m == NULL)
+ continue;
+
+ fp = mtod(m, struct fw_pkt *);
+ plen = fp->mode.stream.len;
+ if (plen < 20 || (uint32_t)m->m_len < 4 + plen) {
+ m_freem(m);
+ continue;
+ }
+
+ pay = (struct fwisound_payload *)(mtod(m, uint8_t *) + 4);
+ if (ntohl(pay->signature) != FWISOUND_SIGNATURE) {
+ m_freem(m);
+ continue;
+ }
+
+ sample_count = ntohl(pay->sample_count);
+ if (sample_count == 0 || sample_count > FWISOUND_MAX_SAMPLES) {
+ m_freem(m);
+ continue;
+ }
+
+ nbytes = sample_count * 4;
+ if (plen < 16 + nbytes) {
+ m_freem(m);
+ continue;
+ }
+
+ if (sc->sample_total != 0 &&
+ ntohl(pay->sample_total) != sc->sample_total)
+ sc->dropped++;
+ sc->sample_total = ntohl(pay->sample_total) + sample_count;
+
+ need_intr = 0;
+ if (chan != NULL && chan->running &&
+ chan->buf != NULL && chan->pcm_chan != NULL) {
+ ring = sndbuf_getbufofs(chan->buf, 0);
+ ring_size = chan->buf->bufsize;
+ old_ptr = chan->hw_ptr;
+
+ uint8_t *src = (uint8_t *)pay->samples;
+ uint32_t rem = nbytes;
+
+ while (rem > 0) {
+ pos = chan->hw_ptr % ring_size;
+ chunk = MIN(rem, ring_size - pos);
+ memcpy(ring + pos, src, chunk);
+ chan->hw_ptr += chunk;
+ src += chunk;
+ rem -= chunk;
+ }
+
+ if (chan->buf->blksz == 0 ||
+ (old_ptr / chan->buf->blksz) !=
+ (chan->hw_ptr / chan->buf->blksz))
+ need_intr = 1;
+ }
+
+ m_freem(m);
+
+ if (need_intr) {
+ chn_intr(chan->pcm_chan);
+ }
+ }
+
+ fw_iso_rearm_done(xferq, sc->fd.fc, &sc->mtx, &sc->iso_active,
+ &sc->dma_ch, dma_ch);
+}
+
+
+static int
+fwisound_probe(device_t dev)
+{
+ struct fw_unit *unit;
+
+ unit = fw_get_unit(dev);
+ if (unit == NULL)
+ return (ENXIO);
+
+ if (unit->spec_id != FWISOUND_SPEC_ID ||
+ unit->sw_version != FWISOUND_VERSION)
+ return (ENXIO);
+
+ device_set_desc(dev, "Apple FireWire Audio");
+ return (BUS_PROBE_DEFAULT);
+}
+
+static int
+fwisound_attach(device_t dev)
+{
+ struct fwisound_softc *sc;
+ struct fw_unit *unit;
+ uint32_t audio_base;
+
+ unit = fw_get_unit(dev);
+ if (unit == NULL || unit->fwdev == NULL)
+ return (ENXIO);
+
+ audio_base = fwisound_find_audio_base(unit);
+ if (audio_base == 0) {
+ device_printf(dev, "no audio base in unit directory\n");
+ return (ENXIO);
+ }
+
+ sc = device_get_softc(dev);
+ sc->fd.dev = dev;
+ sc->fd.fc = fw_get_comm(dev);
+ sc->dev = dev;
+ mtx_init(&sc->mtx, "fwisound", NULL, MTX_DEF);
+
+ sc->fwdev = unit->fwdev;
+ sc->cmd_hi = 0xffff;
+ sc->cmd_lo = 0xf0000000 | (audio_base << 2);
+ sc->state = FWISOUND_STATE_IDLE;
+ sc->dma_ch = -1;
+ sc->iso_active = 0;
+ sc->iso_channel = (uint8_t)(iso_channel & FWXFERQ_CHTAGMASK);
+ sc->chan = NULL;
+ TASK_INIT(&sc->probe_task, 0, fwisound_probe_task, sc);
+ TASK_INIT(&sc->start_task, 0, fwisound_start_task, sc);
+ TASK_INIT(&sc->stop_task, 0, fwisound_stop_task, sc);
+
+ sc->pcm_dev = device_add_child(dev, "pcm", DEVICE_UNIT_ANY);
+ if (sc->pcm_dev == NULL) {
+ device_printf(dev, "failed to add pcm child\n");
+ mtx_destroy(&sc->mtx);
+ return (ENXIO);
+ }
+
+ bus_attach_children(dev);
+
+ if (taskqueue_enqueue(taskqueue_thread, &sc->probe_task) != 0)
+ device_printf(dev, "failed to enqueue probe task\n");
+
+ return (0);
+}
+
+static int
+fwisound_detach(device_t dev)
+{
+ struct fwisound_softc *sc;
+
+ sc = device_get_softc(dev);
+
+ FWISOUND_LOCK(sc);
+ sc->state = FWISOUND_STATE_DETACHING;
+ FWISOUND_UNLOCK(sc);
+
+ taskqueue_drain(taskqueue_thread, &sc->probe_task);
+ taskqueue_drain(taskqueue_thread, &sc->start_task);
+ taskqueue_drain(taskqueue_thread, &sc->stop_task);
+ fwisound_iso_stop(sc);
+
+ if (sc->pcm_dev != NULL)
+ device_delete_children(dev);
+
+ FWISOUND_LOCK(sc);
+ sc->fwdev = NULL;
+ FWISOUND_UNLOCK(sc);
+
+ mtx_destroy(&sc->mtx);
+ return (0);
+}
+
+static device_method_t fwisound_methods[] = {
+ DEVMETHOD(device_probe, fwisound_probe),
+ DEVMETHOD(device_attach, fwisound_attach),
+ DEVMETHOD(device_detach, fwisound_detach),
+
+ DEVMETHOD_END
+};
+
+static driver_t fwisound_driver = {
+ "fwisound",
+ fwisound_methods,
+ sizeof(struct fwisound_softc),
+};
+
+DRIVER_MODULE(fwisound, firewire, fwisound_driver, 0, 0);
+MODULE_VERSION(fwisound, 1);
+MODULE_DEPEND(fwisound, firewire, 1, 1, 1);
+MODULE_DEPEND(fwisound, sound, SOUND_MINVER, SOUND_PREFVER, SOUND_MAXVER);