Add sncodec(4) a driver for the TI SN012776/TAS2764 digital amplifier.
authorkettenis <kettenis@openbsd.org>
Fri, 3 Feb 2023 13:22:59 +0000 (13:22 +0000)
committerkettenis <kettenis@openbsd.org>
Fri, 3 Feb 2023 13:22:59 +0000 (13:22 +0000)
ok ratchov@

sys/dev/fdt/sncodec.c [new file with mode: 0644]

diff --git a/sys/dev/fdt/sncodec.c b/sys/dev/fdt/sncodec.c
new file mode 100644 (file)
index 0000000..bcc656b
--- /dev/null
@@ -0,0 +1,379 @@
+/*     $OpenBSD: sncodec.c,v 1.1 2023/02/03 13:22:59 kettenis Exp $    */
+/*
+ * Copyright (c) 2023 Mark Kettenis <kettenis@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/audioio.h>
+#include <sys/device.h>
+#include <sys/malloc.h>
+
+#include <machine/bus.h>
+
+#include <dev/ofw/openfirm.h>
+#include <dev/ofw/ofw_gpio.h>
+#include <dev/ofw/ofw_misc.h>
+#include <dev/ofw/fdt.h>
+
+#include <dev/i2c/i2cvar.h>
+
+#include <dev/audio_if.h>
+
+#define MODE_CTRL                      0x02
+#define  MODE_CTRL_BOP_SRC             (1 << 7)
+#define  MODE_CTRL_ISNS_PD             (1 << 4)
+#define  MODE_CTRL_VSNS_PD             (1 << 3)
+#define  MODE_CTRL_MODE_ACTIVE         (0 << 0)
+#define  MODE_CTRL_MODE_MUTE           (1 << 0)
+#define  MODE_CTRL_MODE_SHUTDOWN       (2 << 0)
+#define TDM_CFG0                       0x08
+#define  TDM_CFG0_FRAME_START          (1 << 0)
+#define TDM_CFG1                       0x09
+#define  TDM_CFG1_RX_JUSTIFY           (1 << 6)
+#define  TDM_CFG1_RX_OFFSET_MASK       (0x1f << 1)
+#define  TDM_CFG1_RX_OFFSET_SHIFT      1
+#define  TDM_CFG1_RX_EDGE              (1 << 0)
+#define TDM_CFG2                       0x0a
+#define  TDM_CFG2_SCFG_MASK            (3 << 4)
+#define  TDM_CFG2_SCFG_MONO_LEFT       (1 << 4)
+#define  TDM_CFG2_SCFG_MONO_RIGHT      (2 << 4)
+#define  TDM_CFG2_SCFG_STEREO_DOWNMIX  (3 << 4)
+#define TDM_CFG3                       0x0c
+#define  TDM_CFG3_RX_SLOT_R_MASK       0xf0
+#define  TDM_CFG3_RX_SLOT_R_SHIFT      4
+#define  TDM_CFG3_RX_SLOT_L_MASK       0x0f
+#define  TDM_CFG3_RX_SLOT_L_SHIFT      0
+#define DVC                            0x1a
+#define  DVC_PCM_MIN                   0xc9
+#define BOP_CFG0                       0x1d
+
+uint8_t sncodec_bop_cfg[] = {
+       0x01, 0x32, 0x02, 0x22, 0x83, 0x2d, 0x80, 0x02, 0x06,
+       0x32, 0x40, 0x30, 0x02, 0x06, 0x38, 0x40, 0x30, 0x02,
+       0x06, 0x3e, 0x37, 0x30, 0xff, 0xe6
+};
+       
+
+struct sncodec_softc {
+       struct device           sc_dev;
+       i2c_tag_t               sc_tag;
+       i2c_addr_t              sc_addr;
+
+       struct dai_device       sc_dai;
+       uint8_t                 sc_dvc;
+};
+
+int    sncodec_match(struct device *, void *, void *);
+void   sncodec_attach(struct device *, struct device *, void *);
+int    sncodec_activate(struct device *, int);
+
+const struct cfattach sncodec_ca = {
+       sizeof(struct sncodec_softc), sncodec_match, sncodec_attach,
+       NULL, sncodec_activate
+};
+
+struct cfdriver sncodec_cd = {
+       NULL, "sncodec", DV_DULL
+};
+
+int    sncodec_set_format(void *, uint32_t, uint32_t, uint32_t);
+int    sncodec_set_tdm_slot(void *, int);
+
+int    sncodec_set_port(void *, mixer_ctrl_t *);
+int    sncodec_get_port(void *, mixer_ctrl_t *);
+int    sncodec_query_devinfo(void *, mixer_devinfo_t *);
+int    sncodec_trigger_output(void *, void *, void *, int,
+           void (*)(void *), void *, struct audio_params *);
+int    sncodec_halt_output(void *);
+
+const struct audio_hw_if sncodec_hw_if = {
+       .set_port = sncodec_set_port,
+       .get_port = sncodec_get_port,
+       .query_devinfo = sncodec_query_devinfo,
+       .trigger_output = sncodec_trigger_output,
+       .halt_output = sncodec_halt_output,
+};
+
+uint8_t        sncodec_read(struct sncodec_softc *, int);
+void   sncodec_write(struct sncodec_softc *, int, uint8_t);
+
+int
+sncodec_match(struct device *parent, void *match, void *aux)
+{
+       struct i2c_attach_args *ia = aux;
+
+       return iic_is_compatible(ia, "ti,tas2764");
+}
+
+void
+sncodec_attach(struct device *parent, struct device *self, void *aux)
+{
+       struct sncodec_softc *sc = (struct sncodec_softc *)self;
+       struct i2c_attach_args *ia = aux;
+       int node = *(int *)ia->ia_cookie;
+       uint32_t *sdz_gpio;
+       int sdz_gpiolen;
+       uint8_t cfg2;
+       int i;
+
+       sc->sc_tag = ia->ia_tag;
+       sc->sc_addr = ia->ia_addr;
+
+       printf("\n");
+
+       sdz_gpiolen = OF_getproplen(node, "shutdown-gpios");
+       if (sdz_gpiolen > 0) {
+               sdz_gpio = malloc(sdz_gpiolen, M_TEMP, M_WAITOK);
+               OF_getpropintarray(node, "shutdown-gpios",
+                   sdz_gpio, sdz_gpiolen);
+               gpio_controller_config_pin(sdz_gpio, GPIO_CONFIG_OUTPUT);
+               gpio_controller_set_pin(sdz_gpio, 1);
+               free(sdz_gpio, M_TEMP, sdz_gpiolen);
+               delay(1000);
+       }
+
+       sc->sc_dvc = sncodec_read(sc, DVC);
+       if (sc->sc_dvc > DVC_PCM_MIN)
+               sc->sc_dvc = DVC_PCM_MIN;
+
+       /* Default to stereo downmix mode for now. */
+       cfg2 = sncodec_read(sc, TDM_CFG2);
+       cfg2 &= ~TDM_CFG2_SCFG_MASK;
+       cfg2 |= TDM_CFG2_SCFG_STEREO_DOWNMIX;
+       sncodec_write(sc, TDM_CFG2, cfg2);
+
+       /* Configure brownout prevention. */
+       for (i = 0; i < nitems(sncodec_bop_cfg); i++)
+               sncodec_write(sc, BOP_CFG0 + i, sncodec_bop_cfg[i]);
+
+       sc->sc_dai.dd_node = node;
+       sc->sc_dai.dd_cookie = sc;
+       sc->sc_dai.dd_hw_if = &sncodec_hw_if;
+       sc->sc_dai.dd_set_format = sncodec_set_format;
+       sc->sc_dai.dd_set_tdm_slot = sncodec_set_tdm_slot;
+       dai_register(&sc->sc_dai);
+}
+
+int
+sncodec_activate(struct device *self, int act)
+{
+       struct sncodec_softc *sc = (struct sncodec_softc *)self;
+
+       switch (act) {
+       case DVACT_POWERDOWN:
+               sncodec_write(sc, MODE_CTRL, MODE_CTRL_ISNS_PD |
+                   MODE_CTRL_VSNS_PD | MODE_CTRL_MODE_SHUTDOWN);
+               break;
+       }
+
+       return 0;
+}
+
+int
+sncodec_set_format(void *cookie, uint32_t fmt, uint32_t pol,
+    uint32_t clk)
+{
+       struct sncodec_softc *sc = cookie;
+       uint8_t cfg0, cfg1;
+
+       cfg0 = sncodec_read(sc, TDM_CFG0);
+       cfg1 = sncodec_read(sc, TDM_CFG1);
+       cfg1 &= ~TDM_CFG1_RX_OFFSET_MASK;
+
+       switch (fmt) {
+       case DAI_FORMAT_I2S:
+               cfg0 |= TDM_CFG0_FRAME_START;
+               cfg1 &= ~TDM_CFG1_RX_JUSTIFY;
+               cfg1 |= (1 << TDM_CFG1_RX_OFFSET_SHIFT);
+               cfg1 &= ~TDM_CFG1_RX_EDGE;
+               break;
+       case DAI_FORMAT_RJ:
+               cfg0 &= ~TDM_CFG0_FRAME_START;
+               cfg1 |= TDM_CFG1_RX_JUSTIFY;
+               cfg1 &= ~TDM_CFG1_RX_EDGE;
+               break;
+       case DAI_FORMAT_LJ:
+               cfg0 &= ~TDM_CFG0_FRAME_START;
+               cfg1 &= ~TDM_CFG1_RX_JUSTIFY;
+               cfg1 &= ~TDM_CFG1_RX_EDGE;
+               break;
+       default:
+               return EINVAL;
+       }
+
+       if (pol & DAI_POLARITY_IB)
+               cfg1 ^= TDM_CFG1_RX_EDGE;
+       if (pol & DAI_POLARITY_IF)
+               cfg0 ^= TDM_CFG0_FRAME_START;
+
+       if (!(clk & DAI_CLOCK_CBM) || !(clk & DAI_CLOCK_CFM))
+               return EINVAL;
+
+       sncodec_write(sc, TDM_CFG0, cfg0);
+       sncodec_write(sc, TDM_CFG1, cfg1);
+
+       return 0;
+}
+
+int
+sncodec_set_tdm_slot(void *cookie, int slot)
+{
+       struct sncodec_softc *sc = cookie;
+       uint8_t cfg2, cfg3;
+
+       if (slot < 0 || slot >= 16)
+               return EINVAL;
+
+       cfg2 = sncodec_read(sc, TDM_CFG2);
+       cfg3 = sncodec_read(sc, TDM_CFG3);
+       cfg2 &= ~TDM_CFG2_SCFG_MASK;
+       cfg2 |= TDM_CFG2_SCFG_MONO_LEFT;
+       cfg3 &= ~TDM_CFG3_RX_SLOT_L_MASK;
+       cfg3 |= slot << TDM_CFG3_RX_SLOT_L_SHIFT;
+       sncodec_write(sc, TDM_CFG2, cfg2);
+       sncodec_write(sc, TDM_CFG3, cfg3);
+
+       return 0;
+}
+
+/*
+ * Mixer controls; the gain of the TAS2770 is determined by the
+ * amplifier gain and digital volume control setting, but we only
+ * expose the digital volume control setting through the mixer
+ * interface.
+ */
+enum {
+       SNCODEC_MASTER_VOL,
+       SNCODEC_OUTPUT_CLASS
+};
+
+int
+sncodec_set_port(void *priv, mixer_ctrl_t *mc)
+{
+       struct sncodec_softc *sc = priv;
+       u_char level;
+
+       switch (mc->dev) {
+       case SNCODEC_MASTER_VOL:
+               level = mc->un.value.level[AUDIO_MIXER_LEVEL_MONO];
+               sc->sc_dvc = (DVC_PCM_MIN * (255 - level)) / 255;
+               sncodec_write(sc, DVC, sc->sc_dvc);
+               return 0;
+       }
+
+       return EINVAL;
+}
+
+int
+sncodec_get_port(void *priv, mixer_ctrl_t *mc)
+{
+       struct sncodec_softc *sc = priv;
+       u_char level;
+
+       switch (mc->dev) {
+       case SNCODEC_MASTER_VOL:
+               mc->un.value.num_channels = 1;
+               level = 255 - ((255 * sc->sc_dvc) / DVC_PCM_MIN);
+               mc->un.value.level[AUDIO_MIXER_LEVEL_MONO] = level;
+               return 0;
+       }
+
+       return EINVAL;
+}
+
+int
+sncodec_query_devinfo(void *priv, mixer_devinfo_t *di)
+{
+       switch (di->index) {
+       case SNCODEC_MASTER_VOL:
+               di->mixer_class = SNCODEC_OUTPUT_CLASS;
+               di->next = di->prev = AUDIO_MIXER_LAST;
+               strlcpy(di->label.name, AudioNmaster, sizeof(di->label.name));
+               di->type = AUDIO_MIXER_VALUE;
+               di->un.v.num_channels = 1;
+               strlcpy(di->un.v.units.name, AudioNvolume,
+                   sizeof(di->un.v.units.name));
+               return 0;
+
+       case SNCODEC_OUTPUT_CLASS:
+               di->mixer_class = SNCODEC_OUTPUT_CLASS;
+               di->next = di->prev = AUDIO_MIXER_LAST;
+               strlcpy(di->label.name, AudioCoutputs, sizeof(di->label.name));
+               di->type = AUDIO_MIXER_CLASS;
+               return 0;
+       }
+
+       return ENXIO;
+}
+
+int
+sncodec_trigger_output(void *cookie, void *start, void *end, int blksize,
+    void (*intr)(void *), void *intrarg, struct audio_params *params)
+{
+       struct sncodec_softc *sc = cookie;
+
+       sncodec_write(sc, MODE_CTRL, MODE_CTRL_BOP_SRC |
+           MODE_CTRL_ISNS_PD | MODE_CTRL_VSNS_PD | MODE_CTRL_MODE_ACTIVE);
+       return 0;
+}
+
+int
+sncodec_halt_output(void *cookie)
+{
+       struct sncodec_softc *sc = cookie;
+
+       sncodec_write(sc, MODE_CTRL, MODE_CTRL_BOP_SRC |
+           MODE_CTRL_ISNS_PD | MODE_CTRL_VSNS_PD | MODE_CTRL_MODE_SHUTDOWN);
+       return 0;
+}
+
+uint8_t
+sncodec_read(struct sncodec_softc *sc, int reg)
+{
+       uint8_t cmd = reg;
+       uint8_t val;
+       int error;
+
+       iic_acquire_bus(sc->sc_tag, I2C_F_POLL);
+       error = iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP, sc->sc_addr,
+           &cmd, sizeof cmd, &val, sizeof val, I2C_F_POLL);
+       iic_release_bus(sc->sc_tag, I2C_F_POLL);
+
+       if (error) {
+               printf("%s: can't read register 0x%02x\n",
+                   sc->sc_dev.dv_xname, reg);
+               val = 0xff;
+       }
+
+       return val;
+}
+
+void
+sncodec_write(struct sncodec_softc *sc, int reg, uint8_t val)
+{
+       uint8_t cmd = reg;
+       int error;
+
+       iic_acquire_bus(sc->sc_tag, I2C_F_POLL);
+       error = iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr,
+           &cmd, sizeof cmd, &val, sizeof val, I2C_F_POLL);
+       iic_release_bus(sc->sc_tag, I2C_F_POLL);
+
+       if (error) {
+               printf("%s: can't write register 0x%02x\n",
+                   sc->sc_dev.dv_xname, reg);
+       }
+}