v4.19.13 snapshot.
diff --git a/sound/soc/meson/Kconfig b/sound/soc/meson/Kconfig
new file mode 100644
index 0000000..8af8bc3
--- /dev/null
+++ b/sound/soc/meson/Kconfig
@@ -0,0 +1,65 @@
+menu "ASoC support for Amlogic platforms"
+	depends on ARCH_MESON || COMPILE_TEST
+
+config SND_MESON_AXG_FIFO
+	tristate
+	select REGMAP_MMIO
+
+config SND_MESON_AXG_FRDDR
+	tristate "Amlogic AXG Playback FIFO support"
+	select SND_MESON_AXG_FIFO
+	help
+	  Select Y or M to add support for the frontend playback interfaces
+	  embedded in the Amlogic AXG SoC family
+
+config SND_MESON_AXG_TODDR
+	tristate "Amlogic AXG Capture FIFO support"
+	select SND_MESON_AXG_FIFO
+	help
+	  Select Y or M to add support for the frontend capture interfaces
+	  embedded in the Amlogic AXG SoC family
+
+config SND_MESON_AXG_TDM_FORMATTER
+	tristate
+	select REGMAP_MMIO
+
+config SND_MESON_AXG_TDM_INTERFACE
+	tristate
+	select SND_MESON_AXG_TDM_FORMATTER
+
+config SND_MESON_AXG_TDMIN
+	tristate "Amlogic AXG TDM Input Support"
+	select SND_MESON_AXG_TDM_FORMATTER
+	select SND_MESON_AXG_TDM_INTERFACE
+	help
+	  Select Y or M to add support for TDM input formatter embedded
+	  in the Amlogic AXG SoC family
+
+config SND_MESON_AXG_TDMOUT
+	tristate "Amlogic AXG TDM Output Support"
+	select SND_MESON_AXG_TDM_FORMATTER
+	select SND_MESON_AXG_TDM_INTERFACE
+	help
+	  Select Y or M to add support for TDM output formatter embedded
+	  in the Amlogic AXG SoC family
+
+config SND_MESON_AXG_SOUND_CARD
+	tristate "Amlogic AXG Sound Card Support"
+	select SND_MESON_AXG_TDM_INTERFACE
+	imply SND_MESON_AXG_FRDDR
+	imply SND_MESON_AXG_TODDR
+	imply SND_MESON_AXG_TDMIN
+	imply SND_MESON_AXG_TDMOUT
+	imply SND_MESON_AXG_SPDIFOUT
+	help
+	  Select Y or M to add support for the AXG SoC sound card
+
+config SND_MESON_AXG_SPDIFOUT
+	tristate "Amlogic AXG SPDIF Output Support"
+	select SND_PCM_IEC958
+	imply SND_SOC_SPDIF
+	help
+	  Select Y or M to add support for SPDIF output serializer embedded
+	  in the Amlogic AXG SoC family
+
+endmenu
diff --git a/sound/soc/meson/Makefile b/sound/soc/meson/Makefile
new file mode 100644
index 0000000..c5e003b
--- /dev/null
+++ b/sound/soc/meson/Makefile
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: (GPL-2.0 OR MIT)
+
+snd-soc-meson-axg-fifo-objs := axg-fifo.o
+snd-soc-meson-axg-frddr-objs := axg-frddr.o
+snd-soc-meson-axg-toddr-objs := axg-toddr.o
+snd-soc-meson-axg-tdm-formatter-objs := axg-tdm-formatter.o
+snd-soc-meson-axg-tdm-interface-objs := axg-tdm-interface.o
+snd-soc-meson-axg-tdmin-objs := axg-tdmin.o
+snd-soc-meson-axg-tdmout-objs := axg-tdmout.o
+snd-soc-meson-axg-sound-card-objs := axg-card.o
+snd-soc-meson-axg-spdifout-objs := axg-spdifout.o
+
+obj-$(CONFIG_SND_MESON_AXG_FIFO) += snd-soc-meson-axg-fifo.o
+obj-$(CONFIG_SND_MESON_AXG_FRDDR) += snd-soc-meson-axg-frddr.o
+obj-$(CONFIG_SND_MESON_AXG_TODDR) += snd-soc-meson-axg-toddr.o
+obj-$(CONFIG_SND_MESON_AXG_TDM_FORMATTER) += snd-soc-meson-axg-tdm-formatter.o
+obj-$(CONFIG_SND_MESON_AXG_TDM_INTERFACE) += snd-soc-meson-axg-tdm-interface.o
+obj-$(CONFIG_SND_MESON_AXG_TDMIN) += snd-soc-meson-axg-tdmin.o
+obj-$(CONFIG_SND_MESON_AXG_TDMOUT) += snd-soc-meson-axg-tdmout.o
+obj-$(CONFIG_SND_MESON_AXG_SOUND_CARD) += snd-soc-meson-axg-sound-card.o
+obj-$(CONFIG_SND_MESON_AXG_SPDIFOUT) += snd-soc-meson-axg-spdifout.o
diff --git a/sound/soc/meson/axg-card.c b/sound/soc/meson/axg-card.c
new file mode 100644
index 0000000..2914ba0
--- /dev/null
+++ b/sound/soc/meson/axg-card.c
@@ -0,0 +1,671 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-tdm.h"
+
+struct axg_card {
+	struct snd_soc_card card;
+	void **link_data;
+};
+
+struct axg_dai_link_tdm_mask {
+	u32 tx;
+	u32 rx;
+};
+
+struct axg_dai_link_tdm_data {
+	unsigned int mclk_fs;
+	unsigned int slots;
+	unsigned int slot_width;
+	u32 *tx_mask;
+	u32 *rx_mask;
+	struct axg_dai_link_tdm_mask *codec_masks;
+};
+
+#define PREFIX "amlogic,"
+
+static int axg_card_reallocate_links(struct axg_card *priv,
+				     unsigned int num_links)
+{
+	struct snd_soc_dai_link *links;
+	void **ldata;
+
+	links = krealloc(priv->card.dai_link,
+			 num_links * sizeof(*priv->card.dai_link),
+			 GFP_KERNEL | __GFP_ZERO);
+	ldata = krealloc(priv->link_data,
+			 num_links * sizeof(*priv->link_data),
+			 GFP_KERNEL | __GFP_ZERO);
+
+	if (!links || !ldata) {
+		dev_err(priv->card.dev, "failed to allocate links\n");
+		return -ENOMEM;
+	}
+
+	priv->card.dai_link = links;
+	priv->link_data = ldata;
+	priv->card.num_links = num_links;
+	return 0;
+}
+
+static int axg_card_parse_dai(struct snd_soc_card *card,
+			      struct device_node *node,
+			      struct device_node **dai_of_node,
+			      const char **dai_name)
+{
+	struct of_phandle_args args;
+	int ret;
+
+	if (!dai_name || !dai_of_node || !node)
+		return -EINVAL;
+
+	ret = of_parse_phandle_with_args(node, "sound-dai",
+					 "#sound-dai-cells", 0, &args);
+	if (ret) {
+		if (ret != -EPROBE_DEFER)
+			dev_err(card->dev, "can't parse dai %d\n", ret);
+		return ret;
+	}
+	*dai_of_node = args.np;
+
+	return snd_soc_get_dai_name(&args, dai_name);
+}
+
+static int axg_card_set_link_name(struct snd_soc_card *card,
+				  struct snd_soc_dai_link *link,
+				  const char *prefix)
+{
+	char *name = devm_kasprintf(card->dev, GFP_KERNEL, "%s.%s",
+				    prefix, link->cpu_of_node->full_name);
+	if (!name)
+		return -ENOMEM;
+
+	link->name = name;
+	link->stream_name = name;
+
+	return 0;
+}
+
+static void axg_card_clean_references(struct axg_card *priv)
+{
+	struct snd_soc_card *card = &priv->card;
+	struct snd_soc_dai_link *link;
+	int i, j;
+
+	if (card->dai_link) {
+		for (i = 0; i < card->num_links; i++) {
+			link = &card->dai_link[i];
+			of_node_put(link->cpu_of_node);
+			for (j = 0; j < link->num_codecs; j++)
+				of_node_put(link->codecs[j].of_node);
+		}
+	}
+
+	if (card->aux_dev) {
+		for (i = 0; i < card->num_aux_devs; i++)
+			of_node_put(card->aux_dev[i].codec_of_node);
+	}
+
+	kfree(card->dai_link);
+	kfree(priv->link_data);
+}
+
+static int axg_card_add_aux_devices(struct snd_soc_card *card)
+{
+	struct device_node *node = card->dev->of_node;
+	struct snd_soc_aux_dev *aux;
+	int num, i;
+
+	num = of_count_phandle_with_args(node, "audio-aux-devs", NULL);
+	if (num == -ENOENT) {
+		/*
+		 * It is ok to have no auxiliary devices but for this card it
+		 * is a strange situtation. Let's warn the about it.
+		 */
+		dev_warn(card->dev, "card has no auxiliary devices\n");
+		return 0;
+	} else if (num < 0) {
+		dev_err(card->dev, "error getting auxiliary devices: %d\n",
+			num);
+		return num;
+	}
+
+	aux = devm_kcalloc(card->dev, num, sizeof(*aux), GFP_KERNEL);
+	if (!aux)
+		return -ENOMEM;
+	card->aux_dev = aux;
+	card->num_aux_devs = num;
+
+	for (i = 0; i < card->num_aux_devs; i++, aux++) {
+		aux->codec_of_node =
+			of_parse_phandle(node, "audio-aux-devs", i);
+		if (!aux->codec_of_node)
+			return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int axg_card_tdm_be_hw_params(struct snd_pcm_substream *substream,
+				     struct snd_pcm_hw_params *params)
+{
+	struct snd_soc_pcm_runtime *rtd = substream->private_data;
+	struct axg_card *priv = snd_soc_card_get_drvdata(rtd->card);
+	struct axg_dai_link_tdm_data *be =
+		(struct axg_dai_link_tdm_data *)priv->link_data[rtd->num];
+	struct snd_soc_dai *codec_dai;
+	unsigned int mclk;
+	int ret, i;
+
+	if (be->mclk_fs) {
+		mclk = params_rate(params) * be->mclk_fs;
+
+		for (i = 0; i < rtd->num_codecs; i++) {
+			codec_dai = rtd->codec_dais[i];
+			ret = snd_soc_dai_set_sysclk(codec_dai, 0, mclk,
+						     SND_SOC_CLOCK_IN);
+			if (ret && ret != -ENOTSUPP)
+				return ret;
+		}
+
+		ret = snd_soc_dai_set_sysclk(rtd->cpu_dai, 0, mclk,
+					     SND_SOC_CLOCK_OUT);
+		if (ret && ret != -ENOTSUPP)
+			return ret;
+	}
+
+	return 0;
+}
+
+static const struct snd_soc_ops axg_card_tdm_be_ops = {
+	.hw_params = axg_card_tdm_be_hw_params,
+};
+
+static int axg_card_tdm_dai_init(struct snd_soc_pcm_runtime *rtd)
+{
+	struct axg_card *priv = snd_soc_card_get_drvdata(rtd->card);
+	struct axg_dai_link_tdm_data *be =
+		(struct axg_dai_link_tdm_data *)priv->link_data[rtd->num];
+	struct snd_soc_dai *codec_dai;
+	int ret, i;
+
+	for (i = 0; i < rtd->num_codecs; i++) {
+		codec_dai = rtd->codec_dais[i];
+		ret = snd_soc_dai_set_tdm_slot(codec_dai,
+					       be->codec_masks[i].tx,
+					       be->codec_masks[i].rx,
+					       be->slots, be->slot_width);
+		if (ret && ret != -ENOTSUPP) {
+			dev_err(codec_dai->dev,
+				"setting tdm link slots failed\n");
+			return ret;
+		}
+	}
+
+	ret = axg_tdm_set_tdm_slots(rtd->cpu_dai, be->tx_mask, be->rx_mask,
+				    be->slots, be->slot_width);
+	if (ret) {
+		dev_err(rtd->cpu_dai->dev, "setting tdm link slots failed\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int axg_card_tdm_dai_lb_init(struct snd_soc_pcm_runtime *rtd)
+{
+	struct axg_card *priv = snd_soc_card_get_drvdata(rtd->card);
+	struct axg_dai_link_tdm_data *be =
+		(struct axg_dai_link_tdm_data *)priv->link_data[rtd->num];
+	int ret;
+
+	/* The loopback rx_mask is the pad tx_mask */
+	ret = axg_tdm_set_tdm_slots(rtd->cpu_dai, NULL, be->tx_mask,
+				    be->slots, be->slot_width);
+	if (ret) {
+		dev_err(rtd->cpu_dai->dev, "setting tdm link slots failed\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int axg_card_add_tdm_loopback(struct snd_soc_card *card,
+				     int *index)
+{
+	struct axg_card *priv = snd_soc_card_get_drvdata(card);
+	struct snd_soc_dai_link *pad = &card->dai_link[*index];
+	struct snd_soc_dai_link *lb;
+	int ret;
+
+	/* extend links */
+	ret = axg_card_reallocate_links(priv, card->num_links + 1);
+	if (ret)
+		return ret;
+
+	lb = &card->dai_link[*index + 1];
+
+	lb->name = kasprintf(GFP_KERNEL, "%s-lb", pad->name);
+	if (!lb->name)
+		return -ENOMEM;
+
+	lb->stream_name = lb->name;
+	lb->cpu_of_node = pad->cpu_of_node;
+	lb->cpu_dai_name = "TDM Loopback";
+	lb->codec_name = "snd-soc-dummy";
+	lb->codec_dai_name = "snd-soc-dummy-dai";
+	lb->dpcm_capture = 1;
+	lb->no_pcm = 1;
+	lb->ops = &axg_card_tdm_be_ops;
+	lb->init = axg_card_tdm_dai_lb_init;
+
+	/* Provide the same link data to the loopback */
+	priv->link_data[*index + 1] = priv->link_data[*index];
+
+	/*
+	 * axg_card_clean_references() will iterate over this link,
+	 * make sure the node count is balanced
+	 */
+	of_node_get(lb->cpu_of_node);
+
+	/* Let add_links continue where it should */
+	*index += 1;
+
+	return 0;
+}
+
+static unsigned int axg_card_parse_daifmt(struct device_node *node,
+					  struct device_node *cpu_node)
+{
+	struct device_node *bitclkmaster = NULL;
+	struct device_node *framemaster = NULL;
+	unsigned int daifmt;
+
+	daifmt = snd_soc_of_parse_daifmt(node, PREFIX,
+					 &bitclkmaster, &framemaster);
+	daifmt &= ~SND_SOC_DAIFMT_MASTER_MASK;
+
+	/* If no master is provided, default to cpu master */
+	if (!bitclkmaster || bitclkmaster == cpu_node) {
+		daifmt |= (!framemaster || framemaster == cpu_node) ?
+			SND_SOC_DAIFMT_CBS_CFS : SND_SOC_DAIFMT_CBS_CFM;
+	} else {
+		daifmt |= (!framemaster || framemaster == cpu_node) ?
+			SND_SOC_DAIFMT_CBM_CFS : SND_SOC_DAIFMT_CBM_CFM;
+	}
+
+	of_node_put(bitclkmaster);
+	of_node_put(framemaster);
+
+	return daifmt;
+}
+
+static int axg_card_parse_cpu_tdm_slots(struct snd_soc_card *card,
+					struct snd_soc_dai_link *link,
+					struct device_node *node,
+					struct axg_dai_link_tdm_data *be)
+{
+	char propname[32];
+	u32 tx, rx;
+	int i;
+
+	be->tx_mask = devm_kcalloc(card->dev, AXG_TDM_NUM_LANES,
+				   sizeof(*be->tx_mask), GFP_KERNEL);
+	be->rx_mask = devm_kcalloc(card->dev, AXG_TDM_NUM_LANES,
+				   sizeof(*be->rx_mask), GFP_KERNEL);
+	if (!be->tx_mask || !be->rx_mask)
+		return -ENOMEM;
+
+	for (i = 0, tx = 0; i < AXG_TDM_NUM_LANES; i++) {
+		snprintf(propname, 32, "dai-tdm-slot-tx-mask-%d", i);
+		snd_soc_of_get_slot_mask(node, propname, &be->tx_mask[i]);
+		tx = max(tx, be->tx_mask[i]);
+	}
+
+	/* Disable playback is the interface has no tx slots */
+	if (!tx)
+		link->dpcm_playback = 0;
+
+	for (i = 0, rx = 0; i < AXG_TDM_NUM_LANES; i++) {
+		snprintf(propname, 32, "dai-tdm-slot-rx-mask-%d", i);
+		snd_soc_of_get_slot_mask(node, propname, &be->rx_mask[i]);
+		rx = max(rx, be->rx_mask[i]);
+	}
+
+	/* Disable capture is the interface has no rx slots */
+	if (!rx)
+		link->dpcm_capture = 0;
+
+	/* ... but the interface should at least have one of them */
+	if (!tx && !rx) {
+		dev_err(card->dev, "tdm link has no cpu slots\n");
+		return -EINVAL;
+	}
+
+	of_property_read_u32(node, "dai-tdm-slot-num", &be->slots);
+	if (!be->slots) {
+		/*
+		 * If the slot number is not provided, set it such as it
+		 * accommodates the largest mask
+		 */
+		be->slots = fls(max(tx, rx));
+	} else if (be->slots < fls(max(tx, rx)) || be->slots > 32) {
+		/*
+		 * Error if the slots can't accommodate the largest mask or
+		 * if it is just too big
+		 */
+		dev_err(card->dev, "bad slot number\n");
+		return -EINVAL;
+	}
+
+	of_property_read_u32(node, "dai-tdm-slot-width", &be->slot_width);
+
+	return 0;
+}
+
+static int axg_card_parse_codecs_masks(struct snd_soc_card *card,
+				       struct snd_soc_dai_link *link,
+				       struct device_node *node,
+				       struct axg_dai_link_tdm_data *be)
+{
+	struct axg_dai_link_tdm_mask *codec_mask;
+	struct device_node *np;
+
+	codec_mask = devm_kcalloc(card->dev, link->num_codecs,
+				  sizeof(*codec_mask), GFP_KERNEL);
+	if (!codec_mask)
+		return -ENOMEM;
+
+	be->codec_masks = codec_mask;
+
+	for_each_child_of_node(node, np) {
+		snd_soc_of_get_slot_mask(np, "dai-tdm-slot-rx-mask",
+					 &codec_mask->rx);
+		snd_soc_of_get_slot_mask(np, "dai-tdm-slot-tx-mask",
+					 &codec_mask->tx);
+
+		codec_mask++;
+	}
+
+	return 0;
+}
+
+static int axg_card_parse_tdm(struct snd_soc_card *card,
+			      struct device_node *node,
+			      int *index)
+{
+	struct axg_card *priv = snd_soc_card_get_drvdata(card);
+	struct snd_soc_dai_link *link = &card->dai_link[*index];
+	struct axg_dai_link_tdm_data *be;
+	int ret;
+
+	/* Allocate tdm link parameters */
+	be = devm_kzalloc(card->dev, sizeof(*be), GFP_KERNEL);
+	if (!be)
+		return -ENOMEM;
+	priv->link_data[*index] = be;
+
+	/* Setup tdm link */
+	link->ops = &axg_card_tdm_be_ops;
+	link->init = axg_card_tdm_dai_init;
+	link->dai_fmt = axg_card_parse_daifmt(node, link->cpu_of_node);
+
+	of_property_read_u32(node, "mclk-fs", &be->mclk_fs);
+
+	ret = axg_card_parse_cpu_tdm_slots(card, link, node, be);
+	if (ret) {
+		dev_err(card->dev, "error parsing tdm link slots\n");
+		return ret;
+	}
+
+	ret = axg_card_parse_codecs_masks(card, link, node, be);
+	if (ret)
+		return ret;
+
+	/* Add loopback if the pad dai has playback */
+	if (link->dpcm_playback) {
+		ret = axg_card_add_tdm_loopback(card, index);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int axg_card_set_be_link(struct snd_soc_card *card,
+				struct snd_soc_dai_link *link,
+				struct device_node *node)
+{
+	struct snd_soc_dai_link_component *codec;
+	struct device_node *np;
+	int ret, num_codecs;
+
+	link->no_pcm = 1;
+	link->dpcm_playback = 1;
+	link->dpcm_capture = 1;
+
+	num_codecs = of_get_child_count(node);
+	if (!num_codecs) {
+		dev_err(card->dev, "be link %s has no codec\n",
+			node->full_name);
+		return -EINVAL;
+	}
+
+	codec = devm_kcalloc(card->dev, num_codecs, sizeof(*codec), GFP_KERNEL);
+	if (!codec)
+		return -ENOMEM;
+
+	link->codecs = codec;
+	link->num_codecs = num_codecs;
+
+	for_each_child_of_node(node, np) {
+		ret = axg_card_parse_dai(card, np, &codec->of_node,
+					 &codec->dai_name);
+		if (ret) {
+			of_node_put(np);
+			return ret;
+		}
+
+		codec++;
+	}
+
+	ret = axg_card_set_link_name(card, link, "be");
+	if (ret)
+		dev_err(card->dev, "error setting %s link name\n", np->name);
+
+	return ret;
+}
+
+static int axg_card_set_fe_link(struct snd_soc_card *card,
+				struct snd_soc_dai_link *link,
+				bool is_playback)
+{
+	link->dynamic = 1;
+	link->dpcm_merged_format = 1;
+	link->dpcm_merged_chan = 1;
+	link->dpcm_merged_rate = 1;
+	link->codec_dai_name = "snd-soc-dummy-dai";
+	link->codec_name = "snd-soc-dummy";
+
+	if (is_playback)
+		link->dpcm_playback = 1;
+	else
+		link->dpcm_capture = 1;
+
+	return axg_card_set_link_name(card, link, "fe");
+}
+
+static int axg_card_cpu_is_capture_fe(struct device_node *np)
+{
+	return of_device_is_compatible(np, PREFIX "axg-toddr");
+}
+
+static int axg_card_cpu_is_playback_fe(struct device_node *np)
+{
+	return of_device_is_compatible(np, PREFIX "axg-frddr");
+}
+
+static int axg_card_cpu_is_tdm_iface(struct device_node *np)
+{
+	return of_device_is_compatible(np, PREFIX "axg-tdm-iface");
+}
+
+static int axg_card_add_link(struct snd_soc_card *card, struct device_node *np,
+			     int *index)
+{
+	struct snd_soc_dai_link *dai_link = &card->dai_link[*index];
+	int ret;
+
+	ret = axg_card_parse_dai(card, np, &dai_link->cpu_of_node,
+				 &dai_link->cpu_dai_name);
+	if (ret)
+		return ret;
+
+	if (axg_card_cpu_is_playback_fe(dai_link->cpu_of_node))
+		ret = axg_card_set_fe_link(card, dai_link, true);
+	else if (axg_card_cpu_is_capture_fe(dai_link->cpu_of_node))
+		ret = axg_card_set_fe_link(card, dai_link, false);
+	else
+		ret = axg_card_set_be_link(card, dai_link, np);
+
+	if (ret)
+		return ret;
+
+	if (axg_card_cpu_is_tdm_iface(dai_link->cpu_of_node))
+		ret = axg_card_parse_tdm(card, np, index);
+
+	return ret;
+}
+
+static int axg_card_add_links(struct snd_soc_card *card)
+{
+	struct axg_card *priv = snd_soc_card_get_drvdata(card);
+	struct device_node *node = card->dev->of_node;
+	struct device_node *np;
+	int num, i, ret;
+
+	num = of_get_child_count(node);
+	if (!num) {
+		dev_err(card->dev, "card has no links\n");
+		return -EINVAL;
+	}
+
+	ret = axg_card_reallocate_links(priv, num);
+	if (ret)
+		return ret;
+
+	i = 0;
+	for_each_child_of_node(node, np) {
+		ret = axg_card_add_link(card, np, &i);
+		if (ret) {
+			of_node_put(np);
+			return ret;
+		}
+
+		i++;
+	}
+
+	return 0;
+}
+
+static int axg_card_parse_of_optional(struct snd_soc_card *card,
+				      const char *propname,
+				      int (*func)(struct snd_soc_card *c,
+						  const char *p))
+{
+	/* If property is not provided, don't fail ... */
+	if (!of_property_read_bool(card->dev->of_node, propname))
+		return 0;
+
+	/* ... but do fail if it is provided and the parsing fails */
+	return func(card, propname);
+}
+
+static const struct of_device_id axg_card_of_match[] = {
+	{ .compatible = "amlogic,axg-sound-card", },
+	{}
+};
+MODULE_DEVICE_TABLE(of, axg_card_of_match);
+
+static int axg_card_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct axg_card *priv;
+	int ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	platform_set_drvdata(pdev, priv);
+	snd_soc_card_set_drvdata(&priv->card, priv);
+
+	priv->card.owner = THIS_MODULE;
+	priv->card.dev = dev;
+
+	ret = snd_soc_of_parse_card_name(&priv->card, "model");
+	if (ret < 0)
+		return ret;
+
+	ret = axg_card_parse_of_optional(&priv->card, "audio-routing",
+					 snd_soc_of_parse_audio_routing);
+	if (ret) {
+		dev_err(dev, "error while parsing routing\n");
+		return ret;
+	}
+
+	ret = axg_card_parse_of_optional(&priv->card, "audio-widgets",
+					 snd_soc_of_parse_audio_simple_widgets);
+	if (ret) {
+		dev_err(dev, "error while parsing widgets\n");
+		return ret;
+	}
+
+	ret = axg_card_add_links(&priv->card);
+	if (ret)
+		goto out_err;
+
+	ret = axg_card_add_aux_devices(&priv->card);
+	if (ret)
+		goto out_err;
+
+	ret = devm_snd_soc_register_card(dev, &priv->card);
+	if (ret)
+		goto out_err;
+
+	return 0;
+
+out_err:
+	axg_card_clean_references(priv);
+	return ret;
+}
+
+static int axg_card_remove(struct platform_device *pdev)
+{
+	struct axg_card *priv = platform_get_drvdata(pdev);
+
+	axg_card_clean_references(priv);
+
+	return 0;
+}
+
+static struct platform_driver axg_card_pdrv = {
+	.probe = axg_card_probe,
+	.remove = axg_card_remove,
+	.driver = {
+		.name = "axg-sound-card",
+		.of_match_table = axg_card_of_match,
+	},
+};
+module_platform_driver(axg_card_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG ALSA machine driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-fifo.c b/sound/soc/meson/axg-fifo.c
new file mode 100644
index 0000000..3026255
--- /dev/null
+++ b/sound/soc/meson/axg-fifo.c
@@ -0,0 +1,341 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/clk.h>
+#include <linux/of_irq.h>
+#include <linux/of_platform.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/reset.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-fifo.h"
+
+/*
+ * This file implements the platform operations common to the playback and
+ * capture frontend DAI. The logic behind this two types of fifo is very
+ * similar but some difference exist.
+ * These differences the respective DAI drivers
+ */
+
+static struct snd_pcm_hardware axg_fifo_hw = {
+	.info = (SNDRV_PCM_INFO_INTERLEAVED |
+		 SNDRV_PCM_INFO_MMAP |
+		 SNDRV_PCM_INFO_MMAP_VALID |
+		 SNDRV_PCM_INFO_BLOCK_TRANSFER |
+		 SNDRV_PCM_INFO_PAUSE),
+
+	.formats = AXG_FIFO_FORMATS,
+	.rate_min = 5512,
+	.rate_max = 192000,
+	.channels_min = 1,
+	.channels_max = AXG_FIFO_CH_MAX,
+	.period_bytes_min = AXG_FIFO_MIN_DEPTH,
+	.period_bytes_max = UINT_MAX,
+	.periods_min = 2,
+	.periods_max = UINT_MAX,
+
+	/* No real justification for this */
+	.buffer_bytes_max = 1 * 1024 * 1024,
+};
+
+static struct snd_soc_dai *axg_fifo_dai(struct snd_pcm_substream *ss)
+{
+	struct snd_soc_pcm_runtime *rtd = ss->private_data;
+
+	return rtd->cpu_dai;
+}
+
+static struct axg_fifo *axg_fifo_data(struct snd_pcm_substream *ss)
+{
+	struct snd_soc_dai *dai = axg_fifo_dai(ss);
+
+	return snd_soc_dai_get_drvdata(dai);
+}
+
+static struct device *axg_fifo_dev(struct snd_pcm_substream *ss)
+{
+	struct snd_soc_dai *dai = axg_fifo_dai(ss);
+
+	return dai->dev;
+}
+
+static void __dma_enable(struct axg_fifo *fifo,  bool enable)
+{
+	regmap_update_bits(fifo->map, FIFO_CTRL0, CTRL0_DMA_EN,
+			   enable ? CTRL0_DMA_EN : 0);
+}
+
+static int axg_fifo_pcm_trigger(struct snd_pcm_substream *ss, int cmd)
+{
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_RESUME:
+	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+		__dma_enable(fifo, true);
+		break;
+	case SNDRV_PCM_TRIGGER_SUSPEND:
+	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+	case SNDRV_PCM_TRIGGER_STOP:
+		__dma_enable(fifo, false);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static snd_pcm_uframes_t axg_fifo_pcm_pointer(struct snd_pcm_substream *ss)
+{
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+	struct snd_pcm_runtime *runtime = ss->runtime;
+	unsigned int addr;
+
+	regmap_read(fifo->map, FIFO_STATUS2, &addr);
+
+	return bytes_to_frames(runtime, addr - (unsigned int)runtime->dma_addr);
+}
+
+static int axg_fifo_pcm_hw_params(struct snd_pcm_substream *ss,
+				  struct snd_pcm_hw_params *params)
+{
+	struct snd_pcm_runtime *runtime = ss->runtime;
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+	dma_addr_t end_ptr;
+	unsigned int burst_num;
+	int ret;
+
+	ret = snd_pcm_lib_malloc_pages(ss, params_buffer_bytes(params));
+	if (ret < 0)
+		return ret;
+
+	/* Setup dma memory pointers */
+	end_ptr = runtime->dma_addr + runtime->dma_bytes - AXG_FIFO_BURST;
+	regmap_write(fifo->map, FIFO_START_ADDR, runtime->dma_addr);
+	regmap_write(fifo->map, FIFO_FINISH_ADDR, end_ptr);
+
+	/* Setup interrupt periodicity */
+	burst_num = params_period_bytes(params) / AXG_FIFO_BURST;
+	regmap_write(fifo->map, FIFO_INT_ADDR, burst_num);
+
+	/* Enable block count irq */
+	regmap_update_bits(fifo->map, FIFO_CTRL0,
+			   CTRL0_INT_EN(FIFO_INT_COUNT_REPEAT),
+			   CTRL0_INT_EN(FIFO_INT_COUNT_REPEAT));
+
+	return 0;
+}
+
+static int axg_fifo_pcm_hw_free(struct snd_pcm_substream *ss)
+{
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+
+	/* Disable the block count irq */
+	regmap_update_bits(fifo->map, FIFO_CTRL0,
+			   CTRL0_INT_EN(FIFO_INT_COUNT_REPEAT), 0);
+
+	return snd_pcm_lib_free_pages(ss);
+}
+
+static void axg_fifo_ack_irq(struct axg_fifo *fifo, u8 mask)
+{
+	regmap_update_bits(fifo->map, FIFO_CTRL1,
+			   CTRL1_INT_CLR(FIFO_INT_MASK),
+			   CTRL1_INT_CLR(mask));
+
+	/* Clear must also be cleared */
+	regmap_update_bits(fifo->map, FIFO_CTRL1,
+			   CTRL1_INT_CLR(FIFO_INT_MASK),
+			   0);
+}
+
+static irqreturn_t axg_fifo_pcm_irq_block(int irq, void *dev_id)
+{
+	struct snd_pcm_substream *ss = dev_id;
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+	unsigned int status;
+
+	regmap_read(fifo->map, FIFO_STATUS1, &status);
+
+	status = STATUS1_INT_STS(status) & FIFO_INT_MASK;
+	if (status & FIFO_INT_COUNT_REPEAT)
+		snd_pcm_period_elapsed(ss);
+	else
+		dev_dbg(axg_fifo_dev(ss), "unexpected irq - STS 0x%02x\n",
+			status);
+
+	/* Ack irqs */
+	axg_fifo_ack_irq(fifo, status);
+
+	return IRQ_RETVAL(status);
+}
+
+static int axg_fifo_pcm_open(struct snd_pcm_substream *ss)
+{
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+	struct device *dev = axg_fifo_dev(ss);
+	int ret;
+
+	snd_soc_set_runtime_hwparams(ss, &axg_fifo_hw);
+
+	/*
+	 * Make sure the buffer and period size are multiple of the FIFO
+	 * minimum depth size
+	 */
+	ret = snd_pcm_hw_constraint_step(ss->runtime, 0,
+					 SNDRV_PCM_HW_PARAM_BUFFER_BYTES,
+					 AXG_FIFO_MIN_DEPTH);
+	if (ret)
+		return ret;
+
+	ret = snd_pcm_hw_constraint_step(ss->runtime, 0,
+					 SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
+					 AXG_FIFO_MIN_DEPTH);
+	if (ret)
+		return ret;
+
+	ret = request_irq(fifo->irq, axg_fifo_pcm_irq_block, 0,
+			  dev_name(dev), ss);
+
+	/* Enable pclk to access registers and clock the fifo ip */
+	ret = clk_prepare_enable(fifo->pclk);
+	if (ret)
+		return ret;
+
+	/* Setup status2 so it reports the memory pointer */
+	regmap_update_bits(fifo->map, FIFO_CTRL1,
+			   CTRL1_STATUS2_SEL_MASK,
+			   CTRL1_STATUS2_SEL(STATUS2_SEL_DDR_READ));
+
+	/* Make sure the dma is initially disabled */
+	__dma_enable(fifo, false);
+
+	/* Disable irqs until params are ready */
+	regmap_update_bits(fifo->map, FIFO_CTRL0,
+			   CTRL0_INT_EN(FIFO_INT_MASK), 0);
+
+	/* Clear any pending interrupt */
+	axg_fifo_ack_irq(fifo, FIFO_INT_MASK);
+
+	/* Take memory arbitror out of reset */
+	ret = reset_control_deassert(fifo->arb);
+	if (ret)
+		clk_disable_unprepare(fifo->pclk);
+
+	return ret;
+}
+
+static int axg_fifo_pcm_close(struct snd_pcm_substream *ss)
+{
+	struct axg_fifo *fifo = axg_fifo_data(ss);
+	int ret;
+
+	/* Put the memory arbitror back in reset */
+	ret = reset_control_assert(fifo->arb);
+
+	/* Disable fifo ip and register access */
+	clk_disable_unprepare(fifo->pclk);
+
+	/* remove IRQ */
+	free_irq(fifo->irq, ss);
+
+	return ret;
+}
+
+const struct snd_pcm_ops axg_fifo_pcm_ops = {
+	.open =		axg_fifo_pcm_open,
+	.close =        axg_fifo_pcm_close,
+	.ioctl =	snd_pcm_lib_ioctl,
+	.hw_params =	axg_fifo_pcm_hw_params,
+	.hw_free =      axg_fifo_pcm_hw_free,
+	.pointer =	axg_fifo_pcm_pointer,
+	.trigger =	axg_fifo_pcm_trigger,
+};
+EXPORT_SYMBOL_GPL(axg_fifo_pcm_ops);
+
+int axg_fifo_pcm_new(struct snd_soc_pcm_runtime *rtd, unsigned int type)
+{
+	struct snd_card *card = rtd->card->snd_card;
+	size_t size = axg_fifo_hw.buffer_bytes_max;
+
+	return snd_pcm_lib_preallocate_pages(rtd->pcm->streams[type].substream,
+					     SNDRV_DMA_TYPE_DEV, card->dev,
+					     size, size);
+}
+EXPORT_SYMBOL_GPL(axg_fifo_pcm_new);
+
+static const struct regmap_config axg_fifo_regmap_cfg = {
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= FIFO_STATUS2,
+};
+
+int axg_fifo_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct axg_fifo_match_data *data;
+	struct axg_fifo *fifo;
+	struct resource *res;
+	void __iomem *regs;
+
+	data = of_device_get_match_data(dev);
+	if (!data) {
+		dev_err(dev, "failed to match device\n");
+		return -ENODEV;
+	}
+
+	fifo = devm_kzalloc(dev, sizeof(*fifo), GFP_KERNEL);
+	if (!fifo)
+		return -ENOMEM;
+	platform_set_drvdata(pdev, fifo);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	regs = devm_ioremap_resource(dev, res);
+	if (IS_ERR(regs))
+		return PTR_ERR(regs);
+
+	fifo->map = devm_regmap_init_mmio(dev, regs, &axg_fifo_regmap_cfg);
+	if (IS_ERR(fifo->map)) {
+		dev_err(dev, "failed to init regmap: %ld\n",
+			PTR_ERR(fifo->map));
+		return PTR_ERR(fifo->map);
+	}
+
+	fifo->pclk = devm_clk_get(dev, NULL);
+	if (IS_ERR(fifo->pclk)) {
+		if (PTR_ERR(fifo->pclk) != -EPROBE_DEFER)
+			dev_err(dev, "failed to get pclk: %ld\n",
+				PTR_ERR(fifo->pclk));
+		return PTR_ERR(fifo->pclk);
+	}
+
+	fifo->arb = devm_reset_control_get_exclusive(dev, NULL);
+	if (IS_ERR(fifo->arb)) {
+		if (PTR_ERR(fifo->arb) != -EPROBE_DEFER)
+			dev_err(dev, "failed to get arb reset: %ld\n",
+				PTR_ERR(fifo->arb));
+		return PTR_ERR(fifo->arb);
+	}
+
+	fifo->irq = of_irq_get(dev->of_node, 0);
+	if (fifo->irq <= 0) {
+		dev_err(dev, "failed to get irq: %d\n", fifo->irq);
+		return fifo->irq;
+	}
+
+	return devm_snd_soc_register_component(dev, data->component_drv,
+					       data->dai_drv, 1);
+}
+EXPORT_SYMBOL_GPL(axg_fifo_probe);
+
+MODULE_DESCRIPTION("Amlogic AXG fifo driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-fifo.h b/sound/soc/meson/axg-fifo.h
new file mode 100644
index 0000000..cb6c401
--- /dev/null
+++ b/sound/soc/meson/axg-fifo.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: (GPL-2.0 OR MIT) */
+/*
+ * Copyright (c) 2018 BayLibre, SAS.
+ * Author: Jerome Brunet <jbrunet@baylibre.com>
+ */
+
+#ifndef _MESON_AXG_FIFO_H
+#define _MESON_AXG_FIFO_H
+
+struct clk;
+struct platform_device;
+struct regmap;
+struct reset_control;
+
+struct snd_soc_component_driver;
+struct snd_soc_dai;
+struct snd_soc_dai_driver;
+struct snd_pcm_ops;
+struct snd_soc_pcm_runtime;
+
+#define AXG_FIFO_CH_MAX			128
+#define AXG_FIFO_RATES			(SNDRV_PCM_RATE_5512 |		\
+					 SNDRV_PCM_RATE_8000_192000)
+#define AXG_FIFO_FORMATS		(SNDRV_PCM_FMTBIT_S8 |		\
+					 SNDRV_PCM_FMTBIT_S16_LE |	\
+					 SNDRV_PCM_FMTBIT_S20_LE |	\
+					 SNDRV_PCM_FMTBIT_S24_LE |	\
+					 SNDRV_PCM_FMTBIT_S32_LE)
+
+#define AXG_FIFO_BURST			8
+#define AXG_FIFO_MIN_CNT		64
+#define AXG_FIFO_MIN_DEPTH		(AXG_FIFO_BURST * AXG_FIFO_MIN_CNT)
+
+#define FIFO_INT_ADDR_FINISH		BIT(0)
+#define FIFO_INT_ADDR_INT		BIT(1)
+#define FIFO_INT_COUNT_REPEAT		BIT(2)
+#define FIFO_INT_COUNT_ONCE		BIT(3)
+#define FIFO_INT_FIFO_ZERO		BIT(4)
+#define FIFO_INT_FIFO_DEPTH		BIT(5)
+#define FIFO_INT_MASK			GENMASK(7, 0)
+
+#define FIFO_CTRL0			0x00
+#define  CTRL0_DMA_EN			BIT(31)
+#define  CTRL0_INT_EN(x)		((x) << 16)
+#define  CTRL0_SEL_MASK			GENMASK(2, 0)
+#define  CTRL0_SEL_SHIFT		0
+#define FIFO_CTRL1			0x04
+#define  CTRL1_INT_CLR(x)		((x) << 0)
+#define  CTRL1_STATUS2_SEL_MASK		GENMASK(11, 8)
+#define  CTRL1_STATUS2_SEL(x)		((x) << 8)
+#define   STATUS2_SEL_DDR_READ		0
+#define  CTRL1_THRESHOLD_MASK		GENMASK(23, 16)
+#define  CTRL1_THRESHOLD(x)		((x) << 16)
+#define  CTRL1_FRDDR_DEPTH_MASK		GENMASK(31, 24)
+#define  CTRL1_FRDDR_DEPTH(x)		((x) << 24)
+#define FIFO_START_ADDR			0x08
+#define FIFO_FINISH_ADDR		0x0c
+#define FIFO_INT_ADDR			0x10
+#define FIFO_STATUS1			0x14
+#define  STATUS1_INT_STS(x)		((x) << 0)
+#define FIFO_STATUS2			0x18
+
+struct axg_fifo {
+	struct regmap *map;
+	struct clk *pclk;
+	struct reset_control *arb;
+	int irq;
+};
+
+struct axg_fifo_match_data {
+	const struct snd_soc_component_driver *component_drv;
+	struct snd_soc_dai_driver *dai_drv;
+};
+
+extern const struct snd_pcm_ops axg_fifo_pcm_ops;
+
+int axg_fifo_pcm_new(struct snd_soc_pcm_runtime *rtd, unsigned int type);
+int axg_fifo_probe(struct platform_device *pdev);
+
+#endif /* _MESON_AXG_FIFO_H */
diff --git a/sound/soc/meson/axg-frddr.c b/sound/soc/meson/axg-frddr.c
new file mode 100644
index 0000000..a6f6f6a
--- /dev/null
+++ b/sound/soc/meson/axg-frddr.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+/* This driver implements the frontend playback DAI of AXG based SoCs */
+
+#include <linux/clk.h>
+#include <linux/regmap.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-fifo.h"
+
+#define CTRL0_FRDDR_PP_MODE	BIT(30)
+
+static int axg_frddr_dai_startup(struct snd_pcm_substream *substream,
+				 struct snd_soc_dai *dai)
+{
+	struct axg_fifo *fifo = snd_soc_dai_get_drvdata(dai);
+	unsigned int fifo_depth, fifo_threshold;
+	int ret;
+
+	/* Enable pclk to access registers and clock the fifo ip */
+	ret = clk_prepare_enable(fifo->pclk);
+	if (ret)
+		return ret;
+
+	/* Apply single buffer mode to the interface */
+	regmap_update_bits(fifo->map, FIFO_CTRL0, CTRL0_FRDDR_PP_MODE, 0);
+
+	/*
+	 * TODO: We could adapt the fifo depth and the fifo threshold
+	 * depending on the expected memory throughput and lantencies
+	 * For now, we'll just use the same values as the vendor kernel
+	 * Depth and threshold are zero based.
+	 */
+	fifo_depth = AXG_FIFO_MIN_CNT - 1;
+	fifo_threshold = (AXG_FIFO_MIN_CNT / 2) - 1;
+	regmap_update_bits(fifo->map, FIFO_CTRL1,
+			   CTRL1_FRDDR_DEPTH_MASK | CTRL1_THRESHOLD_MASK,
+			   CTRL1_FRDDR_DEPTH(fifo_depth) |
+			   CTRL1_THRESHOLD(fifo_threshold));
+
+	return 0;
+}
+
+static void axg_frddr_dai_shutdown(struct snd_pcm_substream *substream,
+				   struct snd_soc_dai *dai)
+{
+	struct axg_fifo *fifo = snd_soc_dai_get_drvdata(dai);
+
+	clk_disable_unprepare(fifo->pclk);
+}
+
+static int axg_frddr_pcm_new(struct snd_soc_pcm_runtime *rtd,
+			     struct snd_soc_dai *dai)
+{
+	return axg_fifo_pcm_new(rtd, SNDRV_PCM_STREAM_PLAYBACK);
+}
+
+static const struct snd_soc_dai_ops axg_frddr_ops = {
+	.startup	= axg_frddr_dai_startup,
+	.shutdown	= axg_frddr_dai_shutdown,
+};
+
+static struct snd_soc_dai_driver axg_frddr_dai_drv = {
+	.name = "FRDDR",
+	.playback = {
+		.stream_name	= "Playback",
+		.channels_min	= 1,
+		.channels_max	= AXG_FIFO_CH_MAX,
+		.rates		= AXG_FIFO_RATES,
+		.formats	= AXG_FIFO_FORMATS,
+	},
+	.ops		= &axg_frddr_ops,
+	.pcm_new	= axg_frddr_pcm_new,
+};
+
+static const char * const axg_frddr_sel_texts[] = {
+	"OUT 0", "OUT 1", "OUT 2", "OUT 3"
+};
+
+static SOC_ENUM_SINGLE_DECL(axg_frddr_sel_enum, FIFO_CTRL0, CTRL0_SEL_SHIFT,
+			    axg_frddr_sel_texts);
+
+static const struct snd_kcontrol_new axg_frddr_out_demux =
+	SOC_DAPM_ENUM("Output Sink", axg_frddr_sel_enum);
+
+static const struct snd_soc_dapm_widget axg_frddr_dapm_widgets[] = {
+	SND_SOC_DAPM_DEMUX("SINK SEL", SND_SOC_NOPM, 0, 0,
+			   &axg_frddr_out_demux),
+	SND_SOC_DAPM_AIF_OUT("OUT 0", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_OUT("OUT 1", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_OUT("OUT 2", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_OUT("OUT 3", NULL, 0, SND_SOC_NOPM, 0, 0),
+};
+
+static const struct snd_soc_dapm_route axg_frddr_dapm_routes[] = {
+	{ "SINK SEL", NULL, "Playback" },
+	{ "OUT 0", "OUT 0",  "SINK SEL" },
+	{ "OUT 1", "OUT 1",  "SINK SEL" },
+	{ "OUT 2", "OUT 2",  "SINK SEL" },
+	{ "OUT 3", "OUT 3",  "SINK SEL" },
+};
+
+static const struct snd_soc_component_driver axg_frddr_component_drv = {
+	.dapm_widgets		= axg_frddr_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(axg_frddr_dapm_widgets),
+	.dapm_routes		= axg_frddr_dapm_routes,
+	.num_dapm_routes	= ARRAY_SIZE(axg_frddr_dapm_routes),
+	.ops			= &axg_fifo_pcm_ops
+};
+
+static const struct axg_fifo_match_data axg_frddr_match_data = {
+	.component_drv	= &axg_frddr_component_drv,
+	.dai_drv	= &axg_frddr_dai_drv
+};
+
+static const struct of_device_id axg_frddr_of_match[] = {
+	{
+		.compatible = "amlogic,axg-frddr",
+		.data = &axg_frddr_match_data,
+	}, {}
+};
+MODULE_DEVICE_TABLE(of, axg_frddr_of_match);
+
+static struct platform_driver axg_frddr_pdrv = {
+	.probe = axg_fifo_probe,
+	.driver = {
+		.name = "axg-frddr",
+		.of_match_table = axg_frddr_of_match,
+	},
+};
+module_platform_driver(axg_frddr_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG playback fifo driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-spdifout.c b/sound/soc/meson/axg-spdifout.c
new file mode 100644
index 0000000..9dea528
--- /dev/null
+++ b/sound/soc/meson/axg-spdifout.c
@@ -0,0 +1,456 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/clk.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/regmap.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+#include <sound/pcm_params.h>
+#include <sound/pcm_iec958.h>
+
+/*
+ * NOTE:
+ * The meaning of bits SPDIFOUT_CTRL0_XXX_SEL is actually the opposite
+ * of what the documentation says. Manual control on V, U and C bits is
+ * applied when the related sel bits are cleared
+ */
+
+#define SPDIFOUT_STAT			0x00
+#define SPDIFOUT_GAIN0			0x04
+#define SPDIFOUT_GAIN1			0x08
+#define SPDIFOUT_CTRL0			0x0c
+#define  SPDIFOUT_CTRL0_EN		BIT(31)
+#define  SPDIFOUT_CTRL0_RST_OUT		BIT(29)
+#define  SPDIFOUT_CTRL0_RST_IN		BIT(28)
+#define  SPDIFOUT_CTRL0_USEL		BIT(26)
+#define  SPDIFOUT_CTRL0_USET		BIT(25)
+#define  SPDIFOUT_CTRL0_CHSTS_SEL	BIT(24)
+#define  SPDIFOUT_CTRL0_DATA_SEL	BIT(20)
+#define  SPDIFOUT_CTRL0_MSB_FIRST	BIT(19)
+#define  SPDIFOUT_CTRL0_VSEL		BIT(18)
+#define  SPDIFOUT_CTRL0_VSET		BIT(17)
+#define  SPDIFOUT_CTRL0_MASK_MASK	GENMASK(11, 4)
+#define  SPDIFOUT_CTRL0_MASK(x)		((x) << 4)
+#define SPDIFOUT_CTRL1			0x10
+#define  SPDIFOUT_CTRL1_MSB_POS_MASK	GENMASK(12, 8)
+#define  SPDIFOUT_CTRL1_MSB_POS(x)	((x) << 8)
+#define  SPDIFOUT_CTRL1_TYPE_MASK	GENMASK(6, 4)
+#define  SPDIFOUT_CTRL1_TYPE(x)		((x) << 4)
+#define SPDIFOUT_PREAMB			0x14
+#define SPDIFOUT_SWAP			0x18
+#define SPDIFOUT_CHSTS0			0x1c
+#define SPDIFOUT_CHSTS1			0x20
+#define SPDIFOUT_CHSTS2			0x24
+#define SPDIFOUT_CHSTS3			0x28
+#define SPDIFOUT_CHSTS4			0x2c
+#define SPDIFOUT_CHSTS5			0x30
+#define SPDIFOUT_CHSTS6			0x34
+#define SPDIFOUT_CHSTS7			0x38
+#define SPDIFOUT_CHSTS8			0x3c
+#define SPDIFOUT_CHSTS9			0x40
+#define SPDIFOUT_CHSTSA			0x44
+#define SPDIFOUT_CHSTSB			0x48
+#define SPDIFOUT_MUTE_VAL		0x4c
+
+struct axg_spdifout {
+	struct regmap *map;
+	struct clk *mclk;
+	struct clk *pclk;
+};
+
+static void axg_spdifout_enable(struct regmap *map)
+{
+	/* Apply both reset */
+	regmap_update_bits(map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_RST_OUT | SPDIFOUT_CTRL0_RST_IN,
+			   0);
+
+	/* Clear out reset before in reset */
+	regmap_update_bits(map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_RST_OUT, SPDIFOUT_CTRL0_RST_OUT);
+	regmap_update_bits(map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_RST_IN,  SPDIFOUT_CTRL0_RST_IN);
+
+	/* Enable spdifout */
+	regmap_update_bits(map, SPDIFOUT_CTRL0, SPDIFOUT_CTRL0_EN,
+			   SPDIFOUT_CTRL0_EN);
+}
+
+static void axg_spdifout_disable(struct regmap *map)
+{
+	regmap_update_bits(map, SPDIFOUT_CTRL0, SPDIFOUT_CTRL0_EN, 0);
+}
+
+static int axg_spdifout_trigger(struct snd_pcm_substream *substream, int cmd,
+				struct snd_soc_dai *dai)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+
+	switch (cmd) {
+	case SNDRV_PCM_TRIGGER_START:
+	case SNDRV_PCM_TRIGGER_RESUME:
+	case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+		axg_spdifout_enable(priv->map);
+		return 0;
+
+	case SNDRV_PCM_TRIGGER_STOP:
+	case SNDRV_PCM_TRIGGER_SUSPEND:
+	case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+		axg_spdifout_disable(priv->map);
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static int axg_spdifout_digital_mute(struct snd_soc_dai *dai, int mute)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+
+	/* Use spdif valid bit to perform digital mute */
+	regmap_update_bits(priv->map, SPDIFOUT_CTRL0, SPDIFOUT_CTRL0_VSET,
+			   mute ? SPDIFOUT_CTRL0_VSET : 0);
+
+	return 0;
+}
+
+static int axg_spdifout_sample_fmt(struct snd_pcm_hw_params *params,
+				   struct snd_soc_dai *dai)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+	unsigned int val;
+
+	/* Set the samples spdifout will pull from the FIFO */
+	switch (params_channels(params)) {
+	case 1:
+		val = SPDIFOUT_CTRL0_MASK(0x1);
+		break;
+	case 2:
+		val = SPDIFOUT_CTRL0_MASK(0x3);
+		break;
+	default:
+		dev_err(dai->dev, "too many channels for spdif dai: %u\n",
+			params_channels(params));
+		return -EINVAL;
+	}
+
+	regmap_update_bits(priv->map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_MASK_MASK, val);
+
+	/* FIFO data are arranged in chunks of 64bits */
+	switch (params_physical_width(params)) {
+	case 8:
+		/* 8 samples of 8 bits */
+		val = SPDIFOUT_CTRL1_TYPE(0);
+		break;
+	case 16:
+		/* 4 samples of 16 bits - right justified */
+		val = SPDIFOUT_CTRL1_TYPE(2);
+		break;
+	case 32:
+		/* 2 samples of 32 bits - right justified */
+		val = SPDIFOUT_CTRL1_TYPE(4);
+		break;
+	default:
+		dev_err(dai->dev, "Unsupported physical width: %u\n",
+			params_physical_width(params));
+		return -EINVAL;
+	}
+
+	/* Position of the MSB in FIFO samples */
+	val |= SPDIFOUT_CTRL1_MSB_POS(params_width(params) - 1);
+
+	regmap_update_bits(priv->map, SPDIFOUT_CTRL1,
+			   SPDIFOUT_CTRL1_MSB_POS_MASK |
+			   SPDIFOUT_CTRL1_TYPE_MASK, val);
+
+	regmap_update_bits(priv->map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_MSB_FIRST | SPDIFOUT_CTRL0_DATA_SEL,
+			   0);
+
+	return 0;
+}
+
+static int axg_spdifout_set_chsts(struct snd_pcm_hw_params *params,
+				  struct snd_soc_dai *dai)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+	unsigned int offset;
+	int ret;
+	u8 cs[4];
+	u32 val;
+
+	ret = snd_pcm_create_iec958_consumer_hw_params(params, cs, 4);
+	if (ret < 0) {
+		dev_err(dai->dev, "Creating IEC958 channel status failed %d\n",
+			ret);
+		return ret;
+	}
+	val = cs[0] | cs[1] << 8 | cs[2] << 16 | cs[3] << 24;
+
+	/* Setup channel status A bits [31 - 0]*/
+	regmap_write(priv->map, SPDIFOUT_CHSTS0, val);
+
+	/* Clear channel status A bits [191 - 32] */
+	for (offset = SPDIFOUT_CHSTS1; offset <= SPDIFOUT_CHSTS5;
+	     offset += regmap_get_reg_stride(priv->map))
+		regmap_write(priv->map, offset, 0);
+
+	/* Setup channel status B bits [31 - 0]*/
+	regmap_write(priv->map, SPDIFOUT_CHSTS6, val);
+
+	/* Clear channel status B bits [191 - 32] */
+	for (offset = SPDIFOUT_CHSTS7; offset <= SPDIFOUT_CHSTSB;
+	     offset += regmap_get_reg_stride(priv->map))
+		regmap_write(priv->map, offset, 0);
+
+	return 0;
+}
+
+static int axg_spdifout_hw_params(struct snd_pcm_substream *substream,
+				  struct snd_pcm_hw_params *params,
+				  struct snd_soc_dai *dai)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+	unsigned int rate = params_rate(params);
+	int ret;
+
+	/* 2 * 32bits per subframe * 2 channels = 128 */
+	ret = clk_set_rate(priv->mclk, rate * 128);
+	if (ret) {
+		dev_err(dai->dev, "failed to set spdif clock\n");
+		return ret;
+	}
+
+	ret = axg_spdifout_sample_fmt(params, dai);
+	if (ret) {
+		dev_err(dai->dev, "failed to setup sample format\n");
+		return ret;
+	}
+
+	ret = axg_spdifout_set_chsts(params, dai);
+	if (ret) {
+		dev_err(dai->dev, "failed to setup channel status words\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int axg_spdifout_startup(struct snd_pcm_substream *substream,
+				struct snd_soc_dai *dai)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+	int ret;
+
+	/* Clock the spdif output block */
+	ret = clk_prepare_enable(priv->pclk);
+	if (ret) {
+		dev_err(dai->dev, "failed to enable pclk\n");
+		return ret;
+	}
+
+	/* Make sure the block is initially stopped */
+	axg_spdifout_disable(priv->map);
+
+	/* Insert data from bit 27 lsb first */
+	regmap_update_bits(priv->map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_MSB_FIRST | SPDIFOUT_CTRL0_DATA_SEL,
+			   0);
+
+	/* Manual control of V, C and U, U = 0 */
+	regmap_update_bits(priv->map, SPDIFOUT_CTRL0,
+			   SPDIFOUT_CTRL0_CHSTS_SEL | SPDIFOUT_CTRL0_VSEL |
+			   SPDIFOUT_CTRL0_USEL | SPDIFOUT_CTRL0_USET,
+			   0);
+
+	/* Static SWAP configuration ATM */
+	regmap_write(priv->map, SPDIFOUT_SWAP, 0x10);
+
+	return 0;
+}
+
+static void axg_spdifout_shutdown(struct snd_pcm_substream *substream,
+				  struct snd_soc_dai *dai)
+{
+	struct axg_spdifout *priv = snd_soc_dai_get_drvdata(dai);
+
+	clk_disable_unprepare(priv->pclk);
+}
+
+static const struct snd_soc_dai_ops axg_spdifout_ops = {
+	.trigger	= axg_spdifout_trigger,
+	.digital_mute	= axg_spdifout_digital_mute,
+	.hw_params	= axg_spdifout_hw_params,
+	.startup	= axg_spdifout_startup,
+	.shutdown	= axg_spdifout_shutdown,
+};
+
+static struct snd_soc_dai_driver axg_spdifout_dai_drv[] = {
+	{
+		.name = "SPDIF Output",
+		.playback = {
+			.stream_name	= "Playback",
+			.channels_min	= 1,
+			.channels_max	= 2,
+			.rates		= (SNDRV_PCM_RATE_32000  |
+					   SNDRV_PCM_RATE_44100  |
+					   SNDRV_PCM_RATE_48000  |
+					   SNDRV_PCM_RATE_88200  |
+					   SNDRV_PCM_RATE_96000  |
+					   SNDRV_PCM_RATE_176400 |
+					   SNDRV_PCM_RATE_192000),
+			.formats	= (SNDRV_PCM_FMTBIT_S8     |
+					   SNDRV_PCM_FMTBIT_S16_LE |
+					   SNDRV_PCM_FMTBIT_S20_LE |
+					   SNDRV_PCM_FMTBIT_S24_LE),
+		},
+		.ops = &axg_spdifout_ops,
+	},
+};
+
+static const char * const spdifout_sel_texts[] = {
+	"IN 0", "IN 1", "IN 2",
+};
+
+static SOC_ENUM_SINGLE_DECL(axg_spdifout_sel_enum, SPDIFOUT_CTRL1, 24,
+			    spdifout_sel_texts);
+
+static const struct snd_kcontrol_new axg_spdifout_in_mux =
+	SOC_DAPM_ENUM("Input Source", axg_spdifout_sel_enum);
+
+static const struct snd_soc_dapm_widget axg_spdifout_dapm_widgets[] = {
+	SND_SOC_DAPM_AIF_IN("IN 0", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 1", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 2", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_MUX("SRC SEL", SND_SOC_NOPM, 0, 0, &axg_spdifout_in_mux),
+};
+
+static const struct snd_soc_dapm_route axg_spdifout_dapm_routes[] = {
+	{ "SRC SEL", "IN 0", "IN 0" },
+	{ "SRC SEL", "IN 1", "IN 1" },
+	{ "SRC SEL", "IN 2", "IN 2" },
+	{ "Playback", NULL, "SRC SEL" },
+};
+
+static const struct snd_kcontrol_new axg_spdifout_controls[] = {
+	SOC_DOUBLE("Playback Volume", SPDIFOUT_GAIN0,  0,  8, 255, 0),
+	SOC_DOUBLE("Playback Switch", SPDIFOUT_CTRL0, 22, 21, 1, 1),
+	SOC_SINGLE("Playback Gain Enable Switch",
+		   SPDIFOUT_CTRL1, 26, 1, 0),
+	SOC_SINGLE("Playback Channels Mix Switch",
+		   SPDIFOUT_CTRL0, 23, 1, 0),
+};
+
+static int axg_spdifout_set_bias_level(struct snd_soc_component *component,
+				       enum snd_soc_bias_level level)
+{
+	struct axg_spdifout *priv = snd_soc_component_get_drvdata(component);
+	enum snd_soc_bias_level now =
+		snd_soc_component_get_bias_level(component);
+	int ret = 0;
+
+	switch (level) {
+	case SND_SOC_BIAS_PREPARE:
+		if (now == SND_SOC_BIAS_STANDBY)
+			ret = clk_prepare_enable(priv->mclk);
+		break;
+
+	case SND_SOC_BIAS_STANDBY:
+		if (now == SND_SOC_BIAS_PREPARE)
+			clk_disable_unprepare(priv->mclk);
+		break;
+
+	case SND_SOC_BIAS_OFF:
+	case SND_SOC_BIAS_ON:
+		break;
+	}
+
+	return ret;
+}
+
+static const struct snd_soc_component_driver axg_spdifout_component_drv = {
+	.controls		= axg_spdifout_controls,
+	.num_controls		= ARRAY_SIZE(axg_spdifout_controls),
+	.dapm_widgets		= axg_spdifout_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(axg_spdifout_dapm_widgets),
+	.dapm_routes		= axg_spdifout_dapm_routes,
+	.num_dapm_routes	= ARRAY_SIZE(axg_spdifout_dapm_routes),
+	.set_bias_level		= axg_spdifout_set_bias_level,
+};
+
+static const struct regmap_config axg_spdifout_regmap_cfg = {
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= SPDIFOUT_MUTE_VAL,
+};
+
+static const struct of_device_id axg_spdifout_of_match[] = {
+	{ .compatible = "amlogic,axg-spdifout", },
+	{}
+};
+MODULE_DEVICE_TABLE(of, axg_spdifout_of_match);
+
+static int axg_spdifout_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct axg_spdifout *priv;
+	struct resource *res;
+	void __iomem *regs;
+	int ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+	platform_set_drvdata(pdev, priv);
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	regs = devm_ioremap_resource(dev, res);
+	if (IS_ERR(regs))
+		return PTR_ERR(regs);
+
+	priv->map = devm_regmap_init_mmio(dev, regs, &axg_spdifout_regmap_cfg);
+	if (IS_ERR(priv->map)) {
+		dev_err(dev, "failed to init regmap: %ld\n",
+			PTR_ERR(priv->map));
+		return PTR_ERR(priv->map);
+	}
+
+	priv->pclk = devm_clk_get(dev, "pclk");
+	if (IS_ERR(priv->pclk)) {
+		ret = PTR_ERR(priv->pclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get pclk: %d\n", ret);
+		return ret;
+	}
+
+	priv->mclk = devm_clk_get(dev, "mclk");
+	if (IS_ERR(priv->mclk)) {
+		ret = PTR_ERR(priv->mclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get mclk: %d\n", ret);
+		return ret;
+	}
+
+	return devm_snd_soc_register_component(dev, &axg_spdifout_component_drv,
+			axg_spdifout_dai_drv, ARRAY_SIZE(axg_spdifout_dai_drv));
+}
+
+static struct platform_driver axg_spdifout_pdrv = {
+	.probe = axg_spdifout_probe,
+	.driver = {
+		.name = "axg-spdifout",
+		.of_match_table = axg_spdifout_of_match,
+	},
+};
+module_platform_driver(axg_spdifout_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG SPDIF Output driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-tdm-formatter.c b/sound/soc/meson/axg-tdm-formatter.c
new file mode 100644
index 0000000..43e390f
--- /dev/null
+++ b/sound/soc/meson/axg-tdm-formatter.c
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/clk.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/regmap.h>
+#include <sound/soc.h>
+
+#include "axg-tdm-formatter.h"
+
+struct axg_tdm_formatter {
+	struct list_head list;
+	struct axg_tdm_stream *stream;
+	const struct axg_tdm_formatter_driver *drv;
+	struct clk *pclk;
+	struct clk *sclk;
+	struct clk *lrclk;
+	struct clk *sclk_sel;
+	struct clk *lrclk_sel;
+	bool enabled;
+	struct regmap *map;
+};
+
+int axg_tdm_formatter_set_channel_masks(struct regmap *map,
+					struct axg_tdm_stream *ts,
+					unsigned int offset)
+{
+	unsigned int val, ch = ts->channels;
+	unsigned long mask;
+	int i, j;
+
+	/*
+	 * Distribute the channels of the stream over the available slots
+	 * of each TDM lane
+	 */
+	for (i = 0; i < AXG_TDM_NUM_LANES; i++) {
+		val = 0;
+		mask = ts->mask[i];
+
+		for (j = find_first_bit(&mask, 32);
+		     (j < 32) && ch;
+		     j = find_next_bit(&mask, 32, j + 1)) {
+			val |= 1 << j;
+			ch -= 1;
+		}
+
+		regmap_write(map, offset, val);
+		offset += regmap_get_reg_stride(map);
+	}
+
+	/*
+	 * If we still have channel left at the end of the process, it means
+	 * the stream has more channels than we can accommodate and we should
+	 * have caught this earlier.
+	 */
+	if (WARN_ON(ch != 0)) {
+		pr_err("channel mask error\n");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(axg_tdm_formatter_set_channel_masks);
+
+static int axg_tdm_formatter_enable(struct axg_tdm_formatter *formatter)
+{
+	struct axg_tdm_stream *ts = formatter->stream;
+	bool invert = formatter->drv->invert_sclk;
+	int ret;
+
+	/* Do nothing if the formatter is already enabled */
+	if (formatter->enabled)
+		return 0;
+
+	/*
+	 * If sclk is inverted, invert it back and provide the inversion
+	 * required by the formatter
+	 */
+	invert ^= axg_tdm_sclk_invert(ts->iface->fmt);
+	ret = clk_set_phase(formatter->sclk, invert ? 180 : 0);
+	if (ret)
+		return ret;
+
+	/* Setup the stream parameter in the formatter */
+	ret = formatter->drv->ops->prepare(formatter->map, formatter->stream);
+	if (ret)
+		return ret;
+
+	/* Enable the signal clocks feeding the formatter */
+	ret = clk_prepare_enable(formatter->sclk);
+	if (ret)
+		return ret;
+
+	ret = clk_prepare_enable(formatter->lrclk);
+	if (ret) {
+		clk_disable_unprepare(formatter->sclk);
+		return ret;
+	}
+
+	/* Finally, actually enable the formatter */
+	formatter->drv->ops->enable(formatter->map);
+	formatter->enabled = true;
+
+	return 0;
+}
+
+static void axg_tdm_formatter_disable(struct axg_tdm_formatter *formatter)
+{
+	/* Do nothing if the formatter is already disabled */
+	if (!formatter->enabled)
+		return;
+
+	formatter->drv->ops->disable(formatter->map);
+	clk_disable_unprepare(formatter->lrclk);
+	clk_disable_unprepare(formatter->sclk);
+	formatter->enabled = false;
+}
+
+static int axg_tdm_formatter_attach(struct axg_tdm_formatter *formatter)
+{
+	struct axg_tdm_stream *ts = formatter->stream;
+	int ret = 0;
+
+	mutex_lock(&ts->lock);
+
+	/* Catch up if the stream is already running when we attach */
+	if (ts->ready) {
+		ret = axg_tdm_formatter_enable(formatter);
+		if (ret) {
+			pr_err("failed to enable formatter\n");
+			goto out;
+		}
+	}
+
+	list_add_tail(&formatter->list, &ts->formatter_list);
+out:
+	mutex_unlock(&ts->lock);
+	return ret;
+}
+
+static void axg_tdm_formatter_dettach(struct axg_tdm_formatter *formatter)
+{
+	struct axg_tdm_stream *ts = formatter->stream;
+
+	mutex_lock(&ts->lock);
+	list_del(&formatter->list);
+	mutex_unlock(&ts->lock);
+
+	axg_tdm_formatter_disable(formatter);
+}
+
+static int axg_tdm_formatter_power_up(struct axg_tdm_formatter *formatter,
+				      struct snd_soc_dapm_widget *w)
+{
+	struct axg_tdm_stream *ts = formatter->drv->ops->get_stream(w);
+	int ret;
+
+	/*
+	 * If we don't get a stream at this stage, it would mean that the
+	 * widget is powering up but is not attached to any backend DAI.
+	 * It should not happen, ever !
+	 */
+	if (WARN_ON(!ts))
+		return -ENODEV;
+
+	/* Clock our device */
+	ret = clk_prepare_enable(formatter->pclk);
+	if (ret)
+		return ret;
+
+	/* Reparent the bit clock to the TDM interface */
+	ret = clk_set_parent(formatter->sclk_sel, ts->iface->sclk);
+	if (ret)
+		goto disable_pclk;
+
+	/* Reparent the sample clock to the TDM interface */
+	ret = clk_set_parent(formatter->lrclk_sel, ts->iface->lrclk);
+	if (ret)
+		goto disable_pclk;
+
+	formatter->stream = ts;
+	ret = axg_tdm_formatter_attach(formatter);
+	if (ret)
+		goto disable_pclk;
+
+	return 0;
+
+disable_pclk:
+	clk_disable_unprepare(formatter->pclk);
+	return ret;
+}
+
+static void axg_tdm_formatter_power_down(struct axg_tdm_formatter *formatter)
+{
+	axg_tdm_formatter_dettach(formatter);
+	clk_disable_unprepare(formatter->pclk);
+	formatter->stream = NULL;
+}
+
+int axg_tdm_formatter_event(struct snd_soc_dapm_widget *w,
+			    struct snd_kcontrol *control,
+			    int event)
+{
+	struct snd_soc_component *c = snd_soc_dapm_to_component(w->dapm);
+	struct axg_tdm_formatter *formatter = snd_soc_component_get_drvdata(c);
+	int ret = 0;
+
+	switch (event) {
+	case SND_SOC_DAPM_PRE_PMU:
+		ret = axg_tdm_formatter_power_up(formatter, w);
+		break;
+
+	case SND_SOC_DAPM_PRE_PMD:
+		axg_tdm_formatter_power_down(formatter);
+		break;
+
+	default:
+		dev_err(c->dev, "Unexpected event %d\n", event);
+		return -EINVAL;
+	}
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(axg_tdm_formatter_event);
+
+int axg_tdm_formatter_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	const struct axg_tdm_formatter_driver *drv;
+	struct axg_tdm_formatter *formatter;
+	struct resource *res;
+	void __iomem *regs;
+	int ret;
+
+	drv = of_device_get_match_data(dev);
+	if (!drv) {
+		dev_err(dev, "failed to match device\n");
+		return -ENODEV;
+	}
+
+	formatter = devm_kzalloc(dev, sizeof(*formatter), GFP_KERNEL);
+	if (!formatter)
+		return -ENOMEM;
+	platform_set_drvdata(pdev, formatter);
+	formatter->drv = drv;
+
+	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+	regs = devm_ioremap_resource(dev, res);
+	if (IS_ERR(regs))
+		return PTR_ERR(regs);
+
+	formatter->map = devm_regmap_init_mmio(dev, regs, drv->regmap_cfg);
+	if (IS_ERR(formatter->map)) {
+		dev_err(dev, "failed to init regmap: %ld\n",
+			PTR_ERR(formatter->map));
+		return PTR_ERR(formatter->map);
+	}
+
+	/* Peripharal clock */
+	formatter->pclk = devm_clk_get(dev, "pclk");
+	if (IS_ERR(formatter->pclk)) {
+		ret = PTR_ERR(formatter->pclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get pclk: %d\n", ret);
+		return ret;
+	}
+
+	/* Formatter bit clock */
+	formatter->sclk = devm_clk_get(dev, "sclk");
+	if (IS_ERR(formatter->sclk)) {
+		ret = PTR_ERR(formatter->sclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get sclk: %d\n", ret);
+		return ret;
+	}
+
+	/* Formatter sample clock */
+	formatter->lrclk = devm_clk_get(dev, "lrclk");
+	if (IS_ERR(formatter->lrclk)) {
+		ret = PTR_ERR(formatter->lrclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get lrclk: %d\n", ret);
+		return ret;
+	}
+
+	/* Formatter bit clock input multiplexer */
+	formatter->sclk_sel = devm_clk_get(dev, "sclk_sel");
+	if (IS_ERR(formatter->sclk_sel)) {
+		ret = PTR_ERR(formatter->sclk_sel);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get sclk_sel: %d\n", ret);
+		return ret;
+	}
+
+	/* Formatter sample clock input multiplexer */
+	formatter->lrclk_sel = devm_clk_get(dev, "lrclk_sel");
+	if (IS_ERR(formatter->lrclk_sel)) {
+		ret = PTR_ERR(formatter->lrclk_sel);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get lrclk_sel: %d\n", ret);
+		return ret;
+	}
+
+	return devm_snd_soc_register_component(dev, drv->component_drv,
+					       NULL, 0);
+}
+EXPORT_SYMBOL_GPL(axg_tdm_formatter_probe);
+
+int axg_tdm_stream_start(struct axg_tdm_stream *ts)
+{
+	struct axg_tdm_formatter *formatter;
+	int ret = 0;
+
+	mutex_lock(&ts->lock);
+	ts->ready = true;
+
+	/* Start all the formatters attached to the stream */
+	list_for_each_entry(formatter, &ts->formatter_list, list) {
+		ret = axg_tdm_formatter_enable(formatter);
+		if (ret) {
+			pr_err("failed to start tdm stream\n");
+			goto out;
+		}
+	}
+
+out:
+	mutex_unlock(&ts->lock);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(axg_tdm_stream_start);
+
+void axg_tdm_stream_stop(struct axg_tdm_stream *ts)
+{
+	struct axg_tdm_formatter *formatter;
+
+	mutex_lock(&ts->lock);
+	ts->ready = false;
+
+	/* Stop all the formatters attached to the stream */
+	list_for_each_entry(formatter, &ts->formatter_list, list) {
+		axg_tdm_formatter_disable(formatter);
+	}
+
+	mutex_unlock(&ts->lock);
+}
+EXPORT_SYMBOL_GPL(axg_tdm_stream_stop);
+
+struct axg_tdm_stream *axg_tdm_stream_alloc(struct axg_tdm_iface *iface)
+{
+	struct axg_tdm_stream *ts;
+
+	ts = kzalloc(sizeof(*ts), GFP_KERNEL);
+	if (ts) {
+		INIT_LIST_HEAD(&ts->formatter_list);
+		mutex_init(&ts->lock);
+		ts->iface = iface;
+	}
+
+	return ts;
+}
+EXPORT_SYMBOL_GPL(axg_tdm_stream_alloc);
+
+void axg_tdm_stream_free(struct axg_tdm_stream *ts)
+{
+	/*
+	 * If the list is not empty, it would mean that one of the formatter
+	 * widget is still powered and attached to the interface while we
+	 * we are removing the TDM DAI. It should not be possible
+	 */
+	WARN_ON(!list_empty(&ts->formatter_list));
+	mutex_destroy(&ts->lock);
+	kfree(ts);
+}
+EXPORT_SYMBOL_GPL(axg_tdm_stream_free);
+
+MODULE_DESCRIPTION("Amlogic AXG TDM formatter driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-tdm-formatter.h b/sound/soc/meson/axg-tdm-formatter.h
new file mode 100644
index 0000000..cf947ca
--- /dev/null
+++ b/sound/soc/meson/axg-tdm-formatter.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: (GPL-2.0 OR MIT)
+ *
+ * Copyright (c) 2018 Baylibre SAS.
+ * Author: Jerome Brunet <jbrunet@baylibre.com>
+ */
+
+#ifndef _MESON_AXG_TDM_FORMATTER_H
+#define _MESON_AXG_TDM_FORMATTER_H
+
+#include "axg-tdm.h"
+
+struct platform_device;
+struct regmap;
+struct snd_soc_dapm_widget;
+struct snd_kcontrol;
+
+struct axg_tdm_formatter_ops {
+	struct axg_tdm_stream *(*get_stream)(struct snd_soc_dapm_widget *w);
+	void (*enable)(struct regmap *map);
+	void (*disable)(struct regmap *map);
+	int (*prepare)(struct regmap *map, struct axg_tdm_stream *ts);
+};
+
+struct axg_tdm_formatter_driver {
+	const struct snd_soc_component_driver *component_drv;
+	const struct regmap_config *regmap_cfg;
+	const struct axg_tdm_formatter_ops *ops;
+	bool invert_sclk;
+};
+
+int axg_tdm_formatter_set_channel_masks(struct regmap *map,
+					struct axg_tdm_stream *ts,
+					unsigned int offset);
+int axg_tdm_formatter_event(struct snd_soc_dapm_widget *w,
+			    struct snd_kcontrol *control,
+			    int event);
+int axg_tdm_formatter_probe(struct platform_device *pdev);
+
+#endif /* _MESON_AXG_TDM_FORMATTER_H */
diff --git a/sound/soc/meson/axg-tdm-interface.c b/sound/soc/meson/axg-tdm-interface.c
new file mode 100644
index 0000000..7b8baf4
--- /dev/null
+++ b/sound/soc/meson/axg-tdm-interface.c
@@ -0,0 +1,542 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/clk.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-tdm.h"
+
+enum {
+	TDM_IFACE_PAD,
+	TDM_IFACE_LOOPBACK,
+};
+
+static unsigned int axg_tdm_slots_total(u32 *mask)
+{
+	unsigned int slots = 0;
+	int i;
+
+	if (!mask)
+		return 0;
+
+	/* Count the total number of slots provided by all 4 lanes */
+	for (i = 0; i < AXG_TDM_NUM_LANES; i++)
+		slots += hweight32(mask[i]);
+
+	return slots;
+}
+
+int axg_tdm_set_tdm_slots(struct snd_soc_dai *dai, u32 *tx_mask,
+			  u32 *rx_mask, unsigned int slots,
+			  unsigned int slot_width)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	struct axg_tdm_stream *tx = (struct axg_tdm_stream *)
+		dai->playback_dma_data;
+	struct axg_tdm_stream *rx = (struct axg_tdm_stream *)
+		dai->capture_dma_data;
+	unsigned int tx_slots, rx_slots;
+
+	tx_slots = axg_tdm_slots_total(tx_mask);
+	rx_slots = axg_tdm_slots_total(rx_mask);
+
+	/* We should at least have a slot for a valid interface */
+	if (!tx_slots && !rx_slots) {
+		dev_err(dai->dev, "interface has no slot\n");
+		return -EINVAL;
+	}
+
+	/*
+	 * Amend the dai driver channel number and let dpcm channel merge do
+	 * its job
+	 */
+	if (tx) {
+		tx->mask = tx_mask;
+		dai->driver->playback.channels_max = tx_slots;
+	}
+
+	if (rx) {
+		rx->mask = rx_mask;
+		dai->driver->capture.channels_max = rx_slots;
+	}
+
+	iface->slots = slots;
+
+	switch (slot_width) {
+	case 0:
+		/* defaults width to 32 if not provided */
+		iface->slot_width = 32;
+		break;
+	case 8:
+	case 16:
+	case 24:
+	case 32:
+		iface->slot_width = slot_width;
+		break;
+	default:
+		dev_err(dai->dev, "unsupported slot width: %d\n", slot_width);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(axg_tdm_set_tdm_slots);
+
+static int axg_tdm_iface_set_sysclk(struct snd_soc_dai *dai, int clk_id,
+				    unsigned int freq, int dir)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	int ret = -ENOTSUPP;
+
+	if (dir == SND_SOC_CLOCK_OUT && clk_id == 0) {
+		if (!iface->mclk) {
+			dev_warn(dai->dev, "master clock not provided\n");
+		} else {
+			ret = clk_set_rate(iface->mclk, freq);
+			if (!ret)
+				iface->mclk_rate = freq;
+		}
+	}
+
+	return ret;
+}
+
+static int axg_tdm_iface_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+
+	/* These modes are not supported */
+	if (fmt & (SND_SOC_DAIFMT_CBS_CFM | SND_SOC_DAIFMT_CBM_CFS)) {
+		dev_err(dai->dev, "only CBS_CFS and CBM_CFM are supported\n");
+		return -EINVAL;
+	}
+
+	/* If the TDM interface is the clock master, it requires mclk */
+	if (!iface->mclk && (fmt & SND_SOC_DAIFMT_CBS_CFS)) {
+		dev_err(dai->dev, "cpu clock master: mclk missing\n");
+		return -ENODEV;
+	}
+
+	iface->fmt = fmt;
+	return 0;
+}
+
+static int axg_tdm_iface_startup(struct snd_pcm_substream *substream,
+				 struct snd_soc_dai *dai)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	struct axg_tdm_stream *ts =
+		snd_soc_dai_get_dma_data(dai, substream);
+	int ret;
+
+	if (!axg_tdm_slots_total(ts->mask)) {
+		dev_err(dai->dev, "interface has not slots\n");
+		return -EINVAL;
+	}
+
+	/* Apply component wide rate symmetry */
+	if (dai->component->active) {
+		ret = snd_pcm_hw_constraint_single(substream->runtime,
+						   SNDRV_PCM_HW_PARAM_RATE,
+						   iface->rate);
+		if (ret < 0) {
+			dev_err(dai->dev,
+				"can't set iface rate constraint\n");
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+static int axg_tdm_iface_set_stream(struct snd_pcm_substream *substream,
+				    struct snd_pcm_hw_params *params,
+				    struct snd_soc_dai *dai)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	struct axg_tdm_stream *ts = snd_soc_dai_get_dma_data(dai, substream);
+	unsigned int channels = params_channels(params);
+	unsigned int width = params_width(params);
+
+	/* Save rate and sample_bits for component symmetry */
+	iface->rate = params_rate(params);
+
+	/* Make sure this interface can cope with the stream */
+	if (axg_tdm_slots_total(ts->mask) < channels) {
+		dev_err(dai->dev, "not enough slots for channels\n");
+		return -EINVAL;
+	}
+
+	if (iface->slot_width < width) {
+		dev_err(dai->dev, "incompatible slots width for stream\n");
+		return -EINVAL;
+	}
+
+	/* Save the parameter for tdmout/tdmin widgets */
+	ts->physical_width = params_physical_width(params);
+	ts->width = params_width(params);
+	ts->channels = params_channels(params);
+
+	return 0;
+}
+
+static int axg_tdm_iface_set_lrclk(struct snd_soc_dai *dai,
+				   struct snd_pcm_hw_params *params)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	unsigned int ratio_num;
+	int ret;
+
+	ret = clk_set_rate(iface->lrclk, params_rate(params));
+	if (ret) {
+		dev_err(dai->dev, "setting sample clock failed: %d\n", ret);
+		return ret;
+	}
+
+	switch (iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+	case SND_SOC_DAIFMT_LEFT_J:
+	case SND_SOC_DAIFMT_RIGHT_J:
+		/* 50% duty cycle ratio */
+		ratio_num = 1;
+		break;
+
+	case SND_SOC_DAIFMT_DSP_A:
+	case SND_SOC_DAIFMT_DSP_B:
+		/*
+		 * A zero duty cycle ratio will result in setting the mininum
+		 * ratio possible which, for this clock, is 1 cycle of the
+		 * parent bclk clock high and the rest low, This is exactly
+		 * what we want here.
+		 */
+		ratio_num = 0;
+		break;
+
+	default:
+		return -EINVAL;
+	}
+
+	ret = clk_set_duty_cycle(iface->lrclk, ratio_num, 2);
+	if (ret) {
+		dev_err(dai->dev,
+			"setting sample clock duty cycle failed: %d\n", ret);
+		return ret;
+	}
+
+	/* Set sample clock inversion */
+	ret = clk_set_phase(iface->lrclk,
+			    axg_tdm_lrclk_invert(iface->fmt) ? 180 : 0);
+	if (ret) {
+		dev_err(dai->dev,
+			"setting sample clock phase failed: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int axg_tdm_iface_set_sclk(struct snd_soc_dai *dai,
+				  struct snd_pcm_hw_params *params)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	unsigned long srate;
+	int ret;
+
+	srate = iface->slots * iface->slot_width * params_rate(params);
+
+	if (!iface->mclk_rate) {
+		/* If no specific mclk is requested, default to bit clock * 4 */
+		clk_set_rate(iface->mclk, 4 * srate);
+	} else {
+		/* Check if we can actually get the bit clock from mclk */
+		if (iface->mclk_rate % srate) {
+			dev_err(dai->dev,
+				"can't derive sclk %lu from mclk %lu\n",
+				srate, iface->mclk_rate);
+			return -EINVAL;
+		}
+	}
+
+	ret = clk_set_rate(iface->sclk, srate);
+	if (ret) {
+		dev_err(dai->dev, "setting bit clock failed: %d\n", ret);
+		return ret;
+	}
+
+	/* Set the bit clock inversion */
+	ret = clk_set_phase(iface->sclk,
+			    axg_tdm_sclk_invert(iface->fmt) ? 0 : 180);
+	if (ret) {
+		dev_err(dai->dev, "setting bit clock phase failed: %d\n", ret);
+		return ret;
+	}
+
+	return ret;
+}
+
+static int axg_tdm_iface_hw_params(struct snd_pcm_substream *substream,
+				   struct snd_pcm_hw_params *params,
+				   struct snd_soc_dai *dai)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+	int ret;
+
+	switch (iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+	case SND_SOC_DAIFMT_LEFT_J:
+	case SND_SOC_DAIFMT_RIGHT_J:
+		if (iface->slots > 2) {
+			dev_err(dai->dev, "bad slot number for format: %d\n",
+				iface->slots);
+			return -EINVAL;
+		}
+		break;
+
+	case SND_SOC_DAI_FORMAT_DSP_A:
+	case SND_SOC_DAI_FORMAT_DSP_B:
+		break;
+
+	default:
+		dev_err(dai->dev, "unsupported dai format\n");
+		return -EINVAL;
+	}
+
+	ret = axg_tdm_iface_set_stream(substream, params, dai);
+	if (ret)
+		return ret;
+
+	if (iface->fmt & SND_SOC_DAIFMT_CBS_CFS) {
+		ret = axg_tdm_iface_set_sclk(dai, params);
+		if (ret)
+			return ret;
+
+		ret = axg_tdm_iface_set_lrclk(dai, params);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int axg_tdm_iface_hw_free(struct snd_pcm_substream *substream,
+				 struct snd_soc_dai *dai)
+{
+	struct axg_tdm_stream *ts = snd_soc_dai_get_dma_data(dai, substream);
+
+	/* Stop all attached formatters */
+	axg_tdm_stream_stop(ts);
+
+	return 0;
+}
+
+static int axg_tdm_iface_prepare(struct snd_pcm_substream *substream,
+				 struct snd_soc_dai *dai)
+{
+	struct axg_tdm_stream *ts = snd_soc_dai_get_dma_data(dai, substream);
+
+	/* Force all attached formatters to update */
+	return axg_tdm_stream_reset(ts);
+}
+
+static int axg_tdm_iface_remove_dai(struct snd_soc_dai *dai)
+{
+	if (dai->capture_dma_data)
+		axg_tdm_stream_free(dai->capture_dma_data);
+
+	if (dai->playback_dma_data)
+		axg_tdm_stream_free(dai->playback_dma_data);
+
+	return 0;
+}
+
+static int axg_tdm_iface_probe_dai(struct snd_soc_dai *dai)
+{
+	struct axg_tdm_iface *iface = snd_soc_dai_get_drvdata(dai);
+
+	if (dai->capture_widget) {
+		dai->capture_dma_data = axg_tdm_stream_alloc(iface);
+		if (!dai->capture_dma_data)
+			return -ENOMEM;
+	}
+
+	if (dai->playback_widget) {
+		dai->playback_dma_data = axg_tdm_stream_alloc(iface);
+		if (!dai->playback_dma_data) {
+			axg_tdm_iface_remove_dai(dai);
+			return -ENOMEM;
+		}
+	}
+
+	return 0;
+}
+
+static const struct snd_soc_dai_ops axg_tdm_iface_ops = {
+	.set_sysclk	= axg_tdm_iface_set_sysclk,
+	.set_fmt	= axg_tdm_iface_set_fmt,
+	.startup	= axg_tdm_iface_startup,
+	.hw_params	= axg_tdm_iface_hw_params,
+	.prepare	= axg_tdm_iface_prepare,
+	.hw_free	= axg_tdm_iface_hw_free,
+};
+
+/* TDM Backend DAIs */
+static const struct snd_soc_dai_driver axg_tdm_iface_dai_drv[] = {
+	[TDM_IFACE_PAD] = {
+		.name = "TDM Pad",
+		.playback = {
+			.stream_name	= "Playback",
+			.channels_min	= 1,
+			.channels_max	= AXG_TDM_CHANNEL_MAX,
+			.rates		= AXG_TDM_RATES,
+			.formats	= AXG_TDM_FORMATS,
+		},
+		.capture = {
+			.stream_name	= "Capture",
+			.channels_min	= 1,
+			.channels_max	= AXG_TDM_CHANNEL_MAX,
+			.rates		= AXG_TDM_RATES,
+			.formats	= AXG_TDM_FORMATS,
+		},
+		.id = TDM_IFACE_PAD,
+		.ops = &axg_tdm_iface_ops,
+		.probe = axg_tdm_iface_probe_dai,
+		.remove = axg_tdm_iface_remove_dai,
+	},
+	[TDM_IFACE_LOOPBACK] = {
+		.name = "TDM Loopback",
+		.capture = {
+			.stream_name	= "Loopback",
+			.channels_min	= 1,
+			.channels_max	= AXG_TDM_CHANNEL_MAX,
+			.rates		= AXG_TDM_RATES,
+			.formats	= AXG_TDM_FORMATS,
+		},
+		.id = TDM_IFACE_LOOPBACK,
+		.ops = &axg_tdm_iface_ops,
+		.probe = axg_tdm_iface_probe_dai,
+		.remove = axg_tdm_iface_remove_dai,
+	},
+};
+
+static int axg_tdm_iface_set_bias_level(struct snd_soc_component *component,
+					enum snd_soc_bias_level level)
+{
+	struct axg_tdm_iface *iface = snd_soc_component_get_drvdata(component);
+	enum snd_soc_bias_level now =
+		snd_soc_component_get_bias_level(component);
+	int ret = 0;
+
+	switch (level) {
+	case SND_SOC_BIAS_PREPARE:
+		if (now == SND_SOC_BIAS_STANDBY)
+			ret = clk_prepare_enable(iface->mclk);
+		break;
+
+	case SND_SOC_BIAS_STANDBY:
+		if (now == SND_SOC_BIAS_PREPARE)
+			clk_disable_unprepare(iface->mclk);
+		break;
+
+	case SND_SOC_BIAS_OFF:
+	case SND_SOC_BIAS_ON:
+		break;
+	}
+
+	return ret;
+}
+
+static const struct snd_soc_component_driver axg_tdm_iface_component_drv = {
+	.set_bias_level	= axg_tdm_iface_set_bias_level,
+};
+
+static const struct of_device_id axg_tdm_iface_of_match[] = {
+	{ .compatible = "amlogic,axg-tdm-iface", },
+	{}
+};
+MODULE_DEVICE_TABLE(of, axg_tdm_iface_of_match);
+
+static int axg_tdm_iface_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct snd_soc_dai_driver *dai_drv;
+	struct axg_tdm_iface *iface;
+	int ret, i;
+
+	iface = devm_kzalloc(dev, sizeof(*iface), GFP_KERNEL);
+	if (!iface)
+		return -ENOMEM;
+	platform_set_drvdata(pdev, iface);
+
+	/*
+	 * Duplicate dai driver: depending on the slot masks configuration
+	 * We'll change the number of channel provided by DAI stream, so dpcm
+	 * channel merge can be done properly
+	 */
+	dai_drv = devm_kcalloc(dev, ARRAY_SIZE(axg_tdm_iface_dai_drv),
+			       sizeof(*dai_drv), GFP_KERNEL);
+	if (!dai_drv)
+		return -ENOMEM;
+
+	for (i = 0; i < ARRAY_SIZE(axg_tdm_iface_dai_drv); i++)
+		memcpy(&dai_drv[i], &axg_tdm_iface_dai_drv[i],
+		       sizeof(*dai_drv));
+
+	/* Bit clock provided on the pad */
+	iface->sclk = devm_clk_get(dev, "sclk");
+	if (IS_ERR(iface->sclk)) {
+		ret = PTR_ERR(iface->sclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get sclk: %d\n", ret);
+		return ret;
+	}
+
+	/* Sample clock provided on the pad */
+	iface->lrclk = devm_clk_get(dev, "lrclk");
+	if (IS_ERR(iface->lrclk)) {
+		ret = PTR_ERR(iface->lrclk);
+		if (ret != -EPROBE_DEFER)
+			dev_err(dev, "failed to get lrclk: %d\n", ret);
+		return ret;
+	}
+
+	/*
+	 * mclk maybe be missing when the cpu dai is in slave mode and
+	 * the codec does not require it to provide a master clock.
+	 * At this point, ignore the error if mclk is missing. We'll
+	 * throw an error if the cpu dai is master and mclk is missing
+	 */
+	iface->mclk = devm_clk_get(dev, "mclk");
+	if (IS_ERR(iface->mclk)) {
+		ret = PTR_ERR(iface->mclk);
+		if (ret == -ENOENT) {
+			iface->mclk = NULL;
+		} else {
+			if (ret != -EPROBE_DEFER)
+				dev_err(dev, "failed to get mclk: %d\n", ret);
+			return ret;
+		}
+	}
+
+	return devm_snd_soc_register_component(dev,
+					&axg_tdm_iface_component_drv, dai_drv,
+					ARRAY_SIZE(axg_tdm_iface_dai_drv));
+}
+
+static struct platform_driver axg_tdm_iface_pdrv = {
+	.probe = axg_tdm_iface_probe,
+	.driver = {
+		.name = "axg-tdm-iface",
+		.of_match_table = axg_tdm_iface_of_match,
+	},
+};
+module_platform_driver(axg_tdm_iface_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG TDM interface driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-tdm.h b/sound/soc/meson/axg-tdm.h
new file mode 100644
index 0000000..e578b6f
--- /dev/null
+++ b/sound/soc/meson/axg-tdm.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: (GPL-2.0 OR MIT)
+ *
+ * Copyright (c) 2018 Baylibre SAS.
+ * Author: Jerome Brunet <jbrunet@baylibre.com>
+ */
+
+#ifndef _MESON_AXG_TDM_H
+#define _MESON_AXG_TDM_H
+
+#include <linux/clk.h>
+#include <linux/regmap.h>
+#include <sound/pcm.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#define AXG_TDM_NUM_LANES	4
+#define AXG_TDM_CHANNEL_MAX	128
+#define AXG_TDM_RATES		(SNDRV_PCM_RATE_5512 |		\
+				 SNDRV_PCM_RATE_8000_192000)
+#define AXG_TDM_FORMATS		(SNDRV_PCM_FMTBIT_S8 |		\
+				 SNDRV_PCM_FMTBIT_S16_LE |	\
+				 SNDRV_PCM_FMTBIT_S20_LE |	\
+				 SNDRV_PCM_FMTBIT_S24_LE |	\
+				 SNDRV_PCM_FMTBIT_S32_LE)
+
+struct axg_tdm_iface {
+	struct clk *sclk;
+	struct clk *lrclk;
+	struct clk *mclk;
+	unsigned long mclk_rate;
+
+	/* format is common to all the DAIs of the iface */
+	unsigned int fmt;
+	unsigned int slots;
+	unsigned int slot_width;
+
+	/* For component wide symmetry */
+	int rate;
+};
+
+static inline bool axg_tdm_lrclk_invert(unsigned int fmt)
+{
+	return (fmt & SND_SOC_DAIFMT_I2S) ^
+		!!(fmt & (SND_SOC_DAIFMT_IB_IF | SND_SOC_DAIFMT_NB_IF));
+}
+
+static inline bool axg_tdm_sclk_invert(unsigned int fmt)
+{
+	return fmt & (SND_SOC_DAIFMT_IB_IF | SND_SOC_DAIFMT_IB_NF);
+}
+
+struct axg_tdm_stream {
+	struct axg_tdm_iface *iface;
+	struct list_head formatter_list;
+	struct mutex lock;
+	unsigned int channels;
+	unsigned int width;
+	unsigned int physical_width;
+	u32 *mask;
+	bool ready;
+};
+
+struct axg_tdm_stream *axg_tdm_stream_alloc(struct axg_tdm_iface *iface);
+void axg_tdm_stream_free(struct axg_tdm_stream *ts);
+int axg_tdm_stream_start(struct axg_tdm_stream *ts);
+void axg_tdm_stream_stop(struct axg_tdm_stream *ts);
+
+static inline int axg_tdm_stream_reset(struct axg_tdm_stream *ts)
+{
+	axg_tdm_stream_stop(ts);
+	return axg_tdm_stream_start(ts);
+}
+
+int axg_tdm_set_tdm_slots(struct snd_soc_dai *dai, u32 *tx_mask,
+			  u32 *rx_mask, unsigned int slots,
+			  unsigned int slot_width);
+
+#endif /* _MESON_AXG_TDM_H */
diff --git a/sound/soc/meson/axg-tdmin.c b/sound/soc/meson/axg-tdmin.c
new file mode 100644
index 0000000..bbac44c
--- /dev/null
+++ b/sound/soc/meson/axg-tdmin.c
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/regmap.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-tdm-formatter.h"
+
+#define TDMIN_CTRL			0x00
+#define  TDMIN_CTRL_ENABLE		BIT(31)
+#define  TDMIN_CTRL_I2S_MODE		BIT(30)
+#define  TDMIN_CTRL_RST_OUT		BIT(29)
+#define  TDMIN_CTRL_RST_IN		BIT(28)
+#define  TDMIN_CTRL_WS_INV		BIT(25)
+#define  TDMIN_CTRL_SEL_SHIFT		20
+#define  TDMIN_CTRL_IN_BIT_SKEW_MASK	GENMASK(18, 16)
+#define  TDMIN_CTRL_IN_BIT_SKEW(x)	((x) << 16)
+#define  TDMIN_CTRL_LSB_FIRST		BIT(5)
+#define  TDMIN_CTRL_BITNUM_MASK	GENMASK(4, 0)
+#define  TDMIN_CTRL_BITNUM(x)		((x) << 0)
+#define TDMIN_SWAP			0x04
+#define TDMIN_MASK0			0x08
+#define TDMIN_MASK1			0x0c
+#define TDMIN_MASK2			0x10
+#define TDMIN_MASK3			0x14
+#define TDMIN_STAT			0x18
+#define TDMIN_MUTE_VAL			0x1c
+#define TDMIN_MUTE0			0x20
+#define TDMIN_MUTE1			0x24
+#define TDMIN_MUTE2			0x28
+#define TDMIN_MUTE3			0x2c
+
+static const struct regmap_config axg_tdmin_regmap_cfg = {
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= TDMIN_MUTE3,
+};
+
+static const char * const axg_tdmin_sel_texts[] = {
+	"IN 0", "IN 1", "IN 2", "IN 3", "IN 4", "IN 5",
+};
+
+/* Change to special mux control to reset dapm */
+static SOC_ENUM_SINGLE_DECL(axg_tdmin_sel_enum, TDMIN_CTRL,
+			    TDMIN_CTRL_SEL_SHIFT, axg_tdmin_sel_texts);
+
+static const struct snd_kcontrol_new axg_tdmin_in_mux =
+	SOC_DAPM_ENUM("Input Source", axg_tdmin_sel_enum);
+
+static struct snd_soc_dai *
+axg_tdmin_get_be(struct snd_soc_dapm_widget *w)
+{
+	struct snd_soc_dapm_path *p = NULL;
+	struct snd_soc_dai *be;
+
+	snd_soc_dapm_widget_for_each_source_path(w, p) {
+		if (!p->connect)
+			continue;
+
+		if (p->source->id == snd_soc_dapm_dai_out)
+			return (struct snd_soc_dai *)p->source->priv;
+
+		be = axg_tdmin_get_be(p->source);
+		if (be)
+			return be;
+	}
+
+	return NULL;
+}
+
+static struct axg_tdm_stream *
+axg_tdmin_get_tdm_stream(struct snd_soc_dapm_widget *w)
+{
+	struct snd_soc_dai *be = axg_tdmin_get_be(w);
+
+	if (!be)
+		return NULL;
+
+	return be->capture_dma_data;
+}
+
+static void axg_tdmin_enable(struct regmap *map)
+{
+	/* Apply both reset */
+	regmap_update_bits(map, TDMIN_CTRL,
+			   TDMIN_CTRL_RST_OUT | TDMIN_CTRL_RST_IN, 0);
+
+	/* Clear out reset before in reset */
+	regmap_update_bits(map, TDMIN_CTRL,
+			   TDMIN_CTRL_RST_OUT, TDMIN_CTRL_RST_OUT);
+	regmap_update_bits(map, TDMIN_CTRL,
+			   TDMIN_CTRL_RST_IN,  TDMIN_CTRL_RST_IN);
+
+	/* Actually enable tdmin */
+	regmap_update_bits(map, TDMIN_CTRL,
+			   TDMIN_CTRL_ENABLE, TDMIN_CTRL_ENABLE);
+}
+
+static void axg_tdmin_disable(struct regmap *map)
+{
+	regmap_update_bits(map, TDMIN_CTRL, TDMIN_CTRL_ENABLE, 0);
+}
+
+static int axg_tdmin_prepare(struct regmap *map, struct axg_tdm_stream *ts)
+{
+	unsigned int val = 0;
+
+	/* Set stream skew */
+	switch (ts->iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+	case SND_SOC_DAIFMT_DSP_A:
+		val |= TDMIN_CTRL_IN_BIT_SKEW(3);
+		break;
+
+	case SND_SOC_DAIFMT_LEFT_J:
+	case SND_SOC_DAIFMT_RIGHT_J:
+	case SND_SOC_DAIFMT_DSP_B:
+		val = TDMIN_CTRL_IN_BIT_SKEW(2);
+		break;
+
+	default:
+		pr_err("Unsupported format: %u\n",
+		       ts->iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK);
+		return -EINVAL;
+	}
+
+	/* Set stream format mode */
+	switch (ts->iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+	case SND_SOC_DAIFMT_LEFT_J:
+	case SND_SOC_DAIFMT_RIGHT_J:
+		val |= TDMIN_CTRL_I2S_MODE;
+		break;
+	}
+
+	/* If the sample clock is inverted, invert it back for the formatter */
+	if (axg_tdm_lrclk_invert(ts->iface->fmt))
+		val |= TDMIN_CTRL_WS_INV;
+
+	/* Set the slot width */
+	val |= TDMIN_CTRL_BITNUM(ts->iface->slot_width - 1);
+
+	/*
+	 * The following also reset LSB_FIRST which result in the formatter
+	 * placing the first bit received at bit 31
+	 */
+	regmap_update_bits(map, TDMIN_CTRL,
+			   (TDMIN_CTRL_IN_BIT_SKEW_MASK | TDMIN_CTRL_WS_INV |
+			    TDMIN_CTRL_I2S_MODE | TDMIN_CTRL_LSB_FIRST |
+			    TDMIN_CTRL_BITNUM_MASK), val);
+
+	/* Set static swap mask configuration */
+	regmap_write(map, TDMIN_SWAP, 0x76543210);
+
+	return axg_tdm_formatter_set_channel_masks(map, ts, TDMIN_MASK0);
+}
+
+static const struct snd_soc_dapm_widget axg_tdmin_dapm_widgets[] = {
+	SND_SOC_DAPM_AIF_IN("IN 0", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 1", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 2", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 3", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 4", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 5", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_MUX("SRC SEL", SND_SOC_NOPM, 0, 0, &axg_tdmin_in_mux),
+	SND_SOC_DAPM_PGA_E("DEC", SND_SOC_NOPM, 0, 0, NULL, 0,
+			   axg_tdm_formatter_event,
+			   (SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_PRE_PMD)),
+	SND_SOC_DAPM_AIF_OUT("OUT", NULL, 0, SND_SOC_NOPM, 0, 0),
+};
+
+static const struct snd_soc_dapm_route axg_tdmin_dapm_routes[] = {
+	{ "SRC SEL", "IN 0", "IN 0" },
+	{ "SRC SEL", "IN 1", "IN 1" },
+	{ "SRC SEL", "IN 2", "IN 2" },
+	{ "SRC SEL", "IN 3", "IN 3" },
+	{ "SRC SEL", "IN 4", "IN 4" },
+	{ "SRC SEL", "IN 5", "IN 5" },
+	{ "DEC", NULL, "SRC SEL" },
+	{ "OUT", NULL, "DEC" },
+};
+
+static const struct snd_soc_component_driver axg_tdmin_component_drv = {
+	.dapm_widgets		= axg_tdmin_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(axg_tdmin_dapm_widgets),
+	.dapm_routes		= axg_tdmin_dapm_routes,
+	.num_dapm_routes	= ARRAY_SIZE(axg_tdmin_dapm_routes),
+};
+
+static const struct axg_tdm_formatter_ops axg_tdmin_ops = {
+	.get_stream	= axg_tdmin_get_tdm_stream,
+	.prepare	= axg_tdmin_prepare,
+	.enable		= axg_tdmin_enable,
+	.disable	= axg_tdmin_disable,
+};
+
+static const struct axg_tdm_formatter_driver axg_tdmin_drv = {
+	.component_drv	= &axg_tdmin_component_drv,
+	.regmap_cfg	= &axg_tdmin_regmap_cfg,
+	.ops		= &axg_tdmin_ops,
+	.invert_sclk	= false,
+};
+
+static const struct of_device_id axg_tdmin_of_match[] = {
+	{
+		.compatible = "amlogic,axg-tdmin",
+		.data = &axg_tdmin_drv,
+	}, {}
+};
+MODULE_DEVICE_TABLE(of, axg_tdmin_of_match);
+
+static struct platform_driver axg_tdmin_pdrv = {
+	.probe = axg_tdm_formatter_probe,
+	.driver = {
+		.name = "axg-tdmin",
+		.of_match_table = axg_tdmin_of_match,
+	},
+};
+module_platform_driver(axg_tdmin_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG TDM input formatter driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-tdmout.c b/sound/soc/meson/axg-tdmout.c
new file mode 100644
index 0000000..f73368e
--- /dev/null
+++ b/sound/soc/meson/axg-tdmout.c
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <linux/regmap.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-tdm-formatter.h"
+
+#define TDMOUT_CTRL0			0x00
+#define  TDMOUT_CTRL0_BITNUM_MASK	GENMASK(4, 0)
+#define  TDMOUT_CTRL0_BITNUM(x)		((x) << 0)
+#define  TDMOUT_CTRL0_SLOTNUM_MASK	GENMASK(9, 5)
+#define  TDMOUT_CTRL0_SLOTNUM(x)	((x) << 5)
+#define  TDMOUT_CTRL0_INIT_BITNUM_MASK	GENMASK(19, 15)
+#define  TDMOUT_CTRL0_INIT_BITNUM(x)	((x) << 15)
+#define  TDMOUT_CTRL0_ENABLE		BIT(31)
+#define  TDMOUT_CTRL0_RST_OUT		BIT(29)
+#define  TDMOUT_CTRL0_RST_IN		BIT(28)
+#define TDMOUT_CTRL1			0x04
+#define  TDMOUT_CTRL1_TYPE_MASK		GENMASK(6, 4)
+#define  TDMOUT_CTRL1_TYPE(x)		((x) << 4)
+#define  TDMOUT_CTRL1_MSB_POS_MASK	GENMASK(12, 8)
+#define  TDMOUT_CTRL1_MSB_POS(x)	((x) << 8)
+#define  TDMOUT_CTRL1_SEL_SHIFT		24
+#define  TDMOUT_CTRL1_GAIN_EN		26
+#define  TDMOUT_CTRL1_WS_INV		BIT(28)
+#define TDMOUT_SWAP			0x08
+#define TDMOUT_MASK0			0x0c
+#define TDMOUT_MASK1			0x10
+#define TDMOUT_MASK2			0x14
+#define TDMOUT_MASK3			0x18
+#define TDMOUT_STAT			0x1c
+#define TDMOUT_GAIN0			0x20
+#define TDMOUT_GAIN1			0x24
+#define TDMOUT_MUTE_VAL			0x28
+#define TDMOUT_MUTE0			0x2c
+#define TDMOUT_MUTE1			0x30
+#define TDMOUT_MUTE2			0x34
+#define TDMOUT_MUTE3			0x38
+#define TDMOUT_MASK_VAL			0x3c
+
+static const struct regmap_config axg_tdmout_regmap_cfg = {
+	.reg_bits	= 32,
+	.val_bits	= 32,
+	.reg_stride	= 4,
+	.max_register	= TDMOUT_MASK_VAL,
+};
+
+static const struct snd_kcontrol_new axg_tdmout_controls[] = {
+	SOC_DOUBLE("Lane 0 Volume", TDMOUT_GAIN0,  0,  8, 255, 0),
+	SOC_DOUBLE("Lane 1 Volume", TDMOUT_GAIN0, 16, 24, 255, 0),
+	SOC_DOUBLE("Lane 2 Volume", TDMOUT_GAIN1,  0,  8, 255, 0),
+	SOC_DOUBLE("Lane 3 Volume", TDMOUT_GAIN1, 16, 24, 255, 0),
+	SOC_SINGLE("Gain Enable Switch", TDMOUT_CTRL1,
+		   TDMOUT_CTRL1_GAIN_EN, 1, 0),
+};
+
+static const char * const tdmout_sel_texts[] = {
+	"IN 0", "IN 1", "IN 2",
+};
+
+static SOC_ENUM_SINGLE_DECL(axg_tdmout_sel_enum, TDMOUT_CTRL1,
+			    TDMOUT_CTRL1_SEL_SHIFT, tdmout_sel_texts);
+
+static const struct snd_kcontrol_new axg_tdmout_in_mux =
+	SOC_DAPM_ENUM("Input Source", axg_tdmout_sel_enum);
+
+static struct snd_soc_dai *
+axg_tdmout_get_be(struct snd_soc_dapm_widget *w)
+{
+	struct snd_soc_dapm_path *p = NULL;
+	struct snd_soc_dai *be;
+
+	snd_soc_dapm_widget_for_each_sink_path(w, p) {
+		if (!p->connect)
+			continue;
+
+		if (p->sink->id == snd_soc_dapm_dai_in)
+			return (struct snd_soc_dai *)p->sink->priv;
+
+		be = axg_tdmout_get_be(p->sink);
+		if (be)
+			return be;
+	}
+
+	return NULL;
+}
+
+static struct axg_tdm_stream *
+axg_tdmout_get_tdm_stream(struct snd_soc_dapm_widget *w)
+{
+	struct snd_soc_dai *be = axg_tdmout_get_be(w);
+
+	if (!be)
+		return NULL;
+
+	return be->playback_dma_data;
+}
+
+static void axg_tdmout_enable(struct regmap *map)
+{
+	/* Apply both reset */
+	regmap_update_bits(map, TDMOUT_CTRL0,
+			   TDMOUT_CTRL0_RST_OUT | TDMOUT_CTRL0_RST_IN, 0);
+
+	/* Clear out reset before in reset */
+	regmap_update_bits(map, TDMOUT_CTRL0,
+			   TDMOUT_CTRL0_RST_OUT, TDMOUT_CTRL0_RST_OUT);
+	regmap_update_bits(map, TDMOUT_CTRL0,
+			   TDMOUT_CTRL0_RST_IN,  TDMOUT_CTRL0_RST_IN);
+
+	/* Actually enable tdmout */
+	regmap_update_bits(map, TDMOUT_CTRL0,
+			   TDMOUT_CTRL0_ENABLE, TDMOUT_CTRL0_ENABLE);
+}
+
+static void axg_tdmout_disable(struct regmap *map)
+{
+	regmap_update_bits(map, TDMOUT_CTRL0, TDMOUT_CTRL0_ENABLE, 0);
+}
+
+static int axg_tdmout_prepare(struct regmap *map, struct axg_tdm_stream *ts)
+{
+	unsigned int val = 0;
+
+	/* Set the stream skew */
+	switch (ts->iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+	case SND_SOC_DAIFMT_DSP_A:
+		val |= TDMOUT_CTRL0_INIT_BITNUM(1);
+		break;
+
+	case SND_SOC_DAIFMT_LEFT_J:
+	case SND_SOC_DAIFMT_RIGHT_J:
+	case SND_SOC_DAIFMT_DSP_B:
+		val |= TDMOUT_CTRL0_INIT_BITNUM(2);
+		break;
+
+	default:
+		pr_err("Unsupported format: %u\n",
+		       ts->iface->fmt & SND_SOC_DAIFMT_FORMAT_MASK);
+		return -EINVAL;
+	}
+
+	/* Set the slot width */
+	val |= TDMOUT_CTRL0_BITNUM(ts->iface->slot_width - 1);
+
+	/* Set the slot number */
+	val |= TDMOUT_CTRL0_SLOTNUM(ts->iface->slots - 1);
+
+	regmap_update_bits(map, TDMOUT_CTRL0,
+			   TDMOUT_CTRL0_INIT_BITNUM_MASK |
+			   TDMOUT_CTRL0_BITNUM_MASK |
+			   TDMOUT_CTRL0_SLOTNUM_MASK, val);
+
+	/* Set the sample width */
+	val = TDMOUT_CTRL1_MSB_POS(ts->width - 1);
+
+	/* FIFO data are arranged in chunks of 64bits */
+	switch (ts->physical_width) {
+	case 8:
+		/* 8 samples of 8 bits */
+		val |= TDMOUT_CTRL1_TYPE(0);
+		break;
+	case 16:
+		/* 4 samples of 16 bits - right justified */
+		val |= TDMOUT_CTRL1_TYPE(2);
+		break;
+	case 32:
+		/* 2 samples of 32 bits - right justified */
+		val |= TDMOUT_CTRL1_TYPE(4);
+		break;
+	default:
+		pr_err("Unsupported physical width: %u\n",
+		       ts->physical_width);
+		return -EINVAL;
+	}
+
+	/* If the sample clock is inverted, invert it back for the formatter */
+	if (axg_tdm_lrclk_invert(ts->iface->fmt))
+		val |= TDMOUT_CTRL1_WS_INV;
+
+	regmap_update_bits(map, TDMOUT_CTRL1,
+			   (TDMOUT_CTRL1_TYPE_MASK | TDMOUT_CTRL1_MSB_POS_MASK |
+			    TDMOUT_CTRL1_WS_INV), val);
+
+	/* Set static swap mask configuration */
+	regmap_write(map, TDMOUT_SWAP, 0x76543210);
+
+	return axg_tdm_formatter_set_channel_masks(map, ts, TDMOUT_MASK0);
+}
+
+static const struct snd_soc_dapm_widget axg_tdmout_dapm_widgets[] = {
+	SND_SOC_DAPM_AIF_IN("IN 0", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 1", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 2", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_MUX("SRC SEL", SND_SOC_NOPM, 0, 0, &axg_tdmout_in_mux),
+	SND_SOC_DAPM_PGA_E("ENC", SND_SOC_NOPM, 0, 0, NULL, 0,
+			   axg_tdm_formatter_event,
+			   (SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_PRE_PMD)),
+	SND_SOC_DAPM_AIF_OUT("OUT", NULL, 0, SND_SOC_NOPM, 0, 0),
+};
+
+static const struct snd_soc_dapm_route axg_tdmout_dapm_routes[] = {
+	{ "SRC SEL", "IN 0", "IN 0" },
+	{ "SRC SEL", "IN 1", "IN 1" },
+	{ "SRC SEL", "IN 2", "IN 2" },
+	{ "ENC", NULL, "SRC SEL" },
+	{ "OUT", NULL, "ENC" },
+};
+
+static const struct snd_soc_component_driver axg_tdmout_component_drv = {
+	.controls		= axg_tdmout_controls,
+	.num_controls		= ARRAY_SIZE(axg_tdmout_controls),
+	.dapm_widgets		= axg_tdmout_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(axg_tdmout_dapm_widgets),
+	.dapm_routes		= axg_tdmout_dapm_routes,
+	.num_dapm_routes	= ARRAY_SIZE(axg_tdmout_dapm_routes),
+};
+
+static const struct axg_tdm_formatter_ops axg_tdmout_ops = {
+	.get_stream	= axg_tdmout_get_tdm_stream,
+	.prepare	= axg_tdmout_prepare,
+	.enable		= axg_tdmout_enable,
+	.disable	= axg_tdmout_disable,
+};
+
+static const struct axg_tdm_formatter_driver axg_tdmout_drv = {
+	.component_drv	= &axg_tdmout_component_drv,
+	.regmap_cfg	= &axg_tdmout_regmap_cfg,
+	.ops		= &axg_tdmout_ops,
+	.invert_sclk	= true,
+};
+
+static const struct of_device_id axg_tdmout_of_match[] = {
+	{
+		.compatible = "amlogic,axg-tdmout",
+		.data = &axg_tdmout_drv,
+	}, {}
+};
+MODULE_DEVICE_TABLE(of, axg_tdmout_of_match);
+
+static struct platform_driver axg_tdmout_pdrv = {
+	.probe = axg_tdm_formatter_probe,
+	.driver = {
+		.name = "axg-tdmout",
+		.of_match_table = axg_tdmout_of_match,
+	},
+};
+module_platform_driver(axg_tdmout_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG TDM output formatter driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/meson/axg-toddr.c b/sound/soc/meson/axg-toddr.c
new file mode 100644
index 0000000..c2c9bb3
--- /dev/null
+++ b/sound/soc/meson/axg-toddr.c
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+//
+// Copyright (c) 2018 BayLibre, SAS.
+// Author: Jerome Brunet <jbrunet@baylibre.com>
+
+/* This driver implements the frontend capture DAI of AXG based SoCs */
+
+#include <linux/clk.h>
+#include <linux/regmap.h>
+#include <linux/module.h>
+#include <linux/of_platform.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/soc-dai.h>
+
+#include "axg-fifo.h"
+
+#define CTRL0_TODDR_SEL_RESAMPLE	BIT(30)
+#define CTRL0_TODDR_EXT_SIGNED		BIT(29)
+#define CTRL0_TODDR_PP_MODE		BIT(28)
+#define CTRL0_TODDR_TYPE_MASK		GENMASK(15, 13)
+#define CTRL0_TODDR_TYPE(x)		((x) << 13)
+#define CTRL0_TODDR_MSB_POS_MASK	GENMASK(12, 8)
+#define CTRL0_TODDR_MSB_POS(x)		((x) << 8)
+#define CTRL0_TODDR_LSB_POS_MASK	GENMASK(7, 3)
+#define CTRL0_TODDR_LSB_POS(x)		((x) << 3)
+
+static int axg_toddr_pcm_new(struct snd_soc_pcm_runtime *rtd,
+			     struct snd_soc_dai *dai)
+{
+	return axg_fifo_pcm_new(rtd, SNDRV_PCM_STREAM_CAPTURE);
+}
+
+static int axg_toddr_dai_hw_params(struct snd_pcm_substream *substream,
+				   struct snd_pcm_hw_params *params,
+				   struct snd_soc_dai *dai)
+{
+	struct axg_fifo *fifo = snd_soc_dai_get_drvdata(dai);
+	unsigned int type, width, msb = 31;
+
+	/*
+	 * NOTE:
+	 * Almost all backend will place the MSB at bit 31, except SPDIF Input
+	 * which will put it at index 28. When adding support for the SPDIF
+	 * Input, we'll need to find which type of backend we are connected to.
+	 */
+
+	switch (params_physical_width(params)) {
+	case 8:
+		type = 0; /* 8 samples of 8 bits */
+		break;
+	case 16:
+		type = 2; /* 4 samples of 16 bits - right justified */
+		break;
+	case 32:
+		type = 4; /* 2 samples of 32 bits - right justified */
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	width = params_width(params);
+
+	regmap_update_bits(fifo->map, FIFO_CTRL0,
+			   CTRL0_TODDR_TYPE_MASK |
+			   CTRL0_TODDR_MSB_POS_MASK |
+			   CTRL0_TODDR_LSB_POS_MASK,
+			   CTRL0_TODDR_TYPE(type) |
+			   CTRL0_TODDR_MSB_POS(msb) |
+			   CTRL0_TODDR_LSB_POS(msb - (width - 1)));
+
+	return 0;
+}
+
+static int axg_toddr_dai_startup(struct snd_pcm_substream *substream,
+				 struct snd_soc_dai *dai)
+{
+	struct axg_fifo *fifo = snd_soc_dai_get_drvdata(dai);
+	unsigned int fifo_threshold;
+	int ret;
+
+	/* Enable pclk to access registers and clock the fifo ip */
+	ret = clk_prepare_enable(fifo->pclk);
+	if (ret)
+		return ret;
+
+	/* Select orginal data - resampling not supported ATM */
+	regmap_update_bits(fifo->map, FIFO_CTRL0, CTRL0_TODDR_SEL_RESAMPLE, 0);
+
+	/* Only signed format are supported ATM */
+	regmap_update_bits(fifo->map, FIFO_CTRL0, CTRL0_TODDR_EXT_SIGNED,
+			   CTRL0_TODDR_EXT_SIGNED);
+
+	/* Apply single buffer mode to the interface */
+	regmap_update_bits(fifo->map, FIFO_CTRL0, CTRL0_TODDR_PP_MODE, 0);
+
+	/* TODDR does not have a configurable fifo depth */
+	fifo_threshold = AXG_FIFO_MIN_CNT - 1;
+	regmap_update_bits(fifo->map, FIFO_CTRL1, CTRL1_THRESHOLD_MASK,
+			   CTRL1_THRESHOLD(fifo_threshold));
+
+	return 0;
+}
+
+static void axg_toddr_dai_shutdown(struct snd_pcm_substream *substream,
+				   struct snd_soc_dai *dai)
+{
+	struct axg_fifo *fifo = snd_soc_dai_get_drvdata(dai);
+
+	clk_disable_unprepare(fifo->pclk);
+}
+
+static const struct snd_soc_dai_ops axg_toddr_ops = {
+	.hw_params	= axg_toddr_dai_hw_params,
+	.startup	= axg_toddr_dai_startup,
+	.shutdown	= axg_toddr_dai_shutdown,
+};
+
+static struct snd_soc_dai_driver axg_toddr_dai_drv = {
+	.name = "TODDR",
+	.capture = {
+		.stream_name	= "Capture",
+		.channels_min	= 1,
+		.channels_max	= AXG_FIFO_CH_MAX,
+		.rates		= AXG_FIFO_RATES,
+		.formats	= AXG_FIFO_FORMATS,
+	},
+	.ops		= &axg_toddr_ops,
+	.pcm_new	= axg_toddr_pcm_new,
+};
+
+static const char * const axg_toddr_sel_texts[] = {
+	"IN 0", "IN 1", "IN 2", "IN 3", "IN 4", "IN 6"
+};
+
+static const unsigned int axg_toddr_sel_values[] = {
+	0, 1, 2, 3, 4, 6
+};
+
+static SOC_VALUE_ENUM_SINGLE_DECL(axg_toddr_sel_enum, FIFO_CTRL0,
+				  CTRL0_SEL_SHIFT, CTRL0_SEL_MASK,
+				  axg_toddr_sel_texts, axg_toddr_sel_values);
+
+static const struct snd_kcontrol_new axg_toddr_in_mux =
+	SOC_DAPM_ENUM("Input Source", axg_toddr_sel_enum);
+
+static const struct snd_soc_dapm_widget axg_toddr_dapm_widgets[] = {
+	SND_SOC_DAPM_MUX("SRC SEL", SND_SOC_NOPM, 0, 0, &axg_toddr_in_mux),
+	SND_SOC_DAPM_AIF_IN("IN 0", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 1", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 2", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 3", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 4", NULL, 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_AIF_IN("IN 6", NULL, 0, SND_SOC_NOPM, 0, 0),
+};
+
+static const struct snd_soc_dapm_route axg_toddr_dapm_routes[] = {
+	{ "Capture", NULL, "SRC SEL" },
+	{ "SRC SEL", "IN 0", "IN 0" },
+	{ "SRC SEL", "IN 1", "IN 1" },
+	{ "SRC SEL", "IN 2", "IN 2" },
+	{ "SRC SEL", "IN 3", "IN 3" },
+	{ "SRC SEL", "IN 4", "IN 4" },
+	{ "SRC SEL", "IN 6", "IN 6" },
+};
+
+static const struct snd_soc_component_driver axg_toddr_component_drv = {
+	.dapm_widgets		= axg_toddr_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(axg_toddr_dapm_widgets),
+	.dapm_routes		= axg_toddr_dapm_routes,
+	.num_dapm_routes	= ARRAY_SIZE(axg_toddr_dapm_routes),
+	.ops			= &axg_fifo_pcm_ops
+};
+
+static const struct axg_fifo_match_data axg_toddr_match_data = {
+	.component_drv	= &axg_toddr_component_drv,
+	.dai_drv	= &axg_toddr_dai_drv
+};
+
+static const struct of_device_id axg_toddr_of_match[] = {
+	{
+		.compatible = "amlogic,axg-toddr",
+		.data = &axg_toddr_match_data,
+	}, {}
+};
+MODULE_DEVICE_TABLE(of, axg_toddr_of_match);
+
+static struct platform_driver axg_toddr_pdrv = {
+	.probe = axg_fifo_probe,
+	.driver = {
+		.name = "axg-toddr",
+		.of_match_table = axg_toddr_of_match,
+	},
+};
+module_platform_driver(axg_toddr_pdrv);
+
+MODULE_DESCRIPTION("Amlogic AXG capture fifo driver");
+MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>");
+MODULE_LICENSE("GPL v2");