On Tue Jul 21 08:41:13 2026 +0200, Igor Paunovic wrote:
> The Synopsys DesignWare HDMI RX controller extracts the audio stream
> embedded in the incoming HDMI signal and feeds it to an on-SoC I2S
> controller. Expose it as an ALSA capture device by registering the
> generic hdmi-codec as a child of the controller, so that a
> simple-audio-card in the device tree can bind the HDMI RX audio DAI.
> 
> The sample rate is recovered from the ACR N/CTS values together with the
> measured TMDS character rate. A periodic worker keeps the local audio
> reference clock locked to the source by nudging it in small ppm steps to
> hold the audio FIFO fill level near its target, which avoids FIFO
> under/overflow and the resulting dropped samples.
> 
> Across system suspend the worker is stopped before the controller
> clocks are gated. Resume fully resets the controller, wiping the audio
> configuration, so when a capture stream is active it re-programs the
> audio path from the last known sample rate and re-arms the worker,
> whose rate change and FIFO error paths resynchronize once the source
> delivers audio again.
> 
> Signed-off-by: Igor Paunovic <[email protected]>
> Reviewed-by: Dmitry Osipenko <[email protected]>
> Tested-by: Dmitry Osipenko <[email protected]>
> Signed-off-by: Hans Verkuil <[email protected]>

Patch committed.

Thanks,
Hans Verkuil

 .../media/platform/synopsys/hdmirx/snps_hdmirx.c   | 306 +++++++++++++++++++++
 .../media/platform/synopsys/hdmirx/snps_hdmirx.h   |   8 +
 2 files changed, 314 insertions(+)

---

diff --git a/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c 
b/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c
index e84d4418c530..978bc91d3257 100644
--- a/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c
+++ b/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.c
@@ -41,6 +41,8 @@
 #include <media/videobuf2-dma-contig.h>
 #include <media/videobuf2-v4l2.h>
 
+#include <sound/hdmi-codec.h>
+
 #include "snps_hdmirx.h"
 #include "snps_hdmirx_cec.h"
 
@@ -132,6 +134,13 @@ struct snps_hdmirx_dev {
        struct delayed_work delayed_work_hotplug;
        struct delayed_work delayed_work_res_change;
        struct hdmirx_cec *cec;
+       struct platform_device *audio_pdev;
+       struct clk *audio_clk;
+       struct delayed_work audio_work;
+       u32 audio_clkrate;
+       u32 audio_fs;
+       int audio_pre_state;
+       bool audio_streaming;
        struct mutex phy_rw_lock; /* to protect phy r/w configuration */
        struct mutex stream_lock; /* to lock video stream capture */
        struct mutex work_lock; /* to lock the critical section of hotplug 
event */
@@ -2279,6 +2288,13 @@ static int hdmirx_parse_dt(struct snps_hdmirx_dev 
*hdmirx_dev)
        if (hdmirx_dev->num_clks < 1)
                return -ENODEV;
 
+       for (int i = 0; i < hdmirx_dev->num_clks; i++) {
+               if (!strcmp(hdmirx_dev->clks[i].id, "audio")) {
+                       hdmirx_dev->audio_clk = hdmirx_dev->clks[i].clk;
+                       break;
+               }
+       }
+
        hdmirx_dev->resets[HDMIRX_RST_A].id = "axi";
        hdmirx_dev->resets[HDMIRX_RST_P].id = "apb";
        hdmirx_dev->resets[HDMIRX_RST_REF].id = "ref";
@@ -2523,10 +2539,19 @@ static void hdmirx_enable_irq(struct device *dev)
                           msecs_to_jiffies(110));
 }
 
+static void hdmirx_audio_setup(struct snps_hdmirx_dev *hdmirx_dev, u32 fs);
+
 static __maybe_unused int hdmirx_suspend(struct device *dev)
 {
        struct snps_hdmirx_dev *hdmirx_dev = dev_get_drvdata(dev);
 
+       /*
+        * Stop the audio worker before the controller clocks are gated;
+        * the audio path is restored and the worker re-armed from
+        * resume() while a capture stream is active.
+        */
+       cancel_delayed_work_sync(&hdmirx_dev->audio_work);
+
        hdmirx_disable_irq(dev);
 
        /* TODO store CEC HW state */
@@ -2549,6 +2574,19 @@ static __maybe_unused int hdmirx_resume(struct device 
*dev)
                hdmirx_hpd_ctrl(hdmirx_dev, true);
        }
 
+       /*
+        * hdmirx_enable() fully reset the controller, wiping the audio
+        * configuration. If a capture stream is active across suspend,
+        * re-program the audio path with the last known sample rate and
+        * restart the worker; its rate change and FIFO error paths
+        * resynchronize once the source delivers audio again.
+        */
+       if (READ_ONCE(hdmirx_dev->audio_streaming)) {
+               hdmirx_audio_setup(hdmirx_dev, hdmirx_dev->audio_fs);
+               mod_delayed_work(system_unbound_wq, &hdmirx_dev->audio_work,
+                                msecs_to_jiffies(200));
+       }
+
        /* TODO restore CEC HW state */
        enable_irq(hdmirx_dev->cec->irq);
 
@@ -2647,6 +2685,266 @@ static int hdmirx_register_cec(struct snps_hdmirx_dev 
*hdmirx_dev,
        return 0;
 }
 
+#define HDMIRX_AUDIO_INIT_FIFO_STATE   128
+#define HDMIRX_AUDIO_INIT_STATE                (HDMIRX_AUDIO_INIT_FIFO_STATE * 
4)
+
+static const int hdmirx_supported_fs[] = {
+       32000, 44100, 48000, 88200, 96000, 176400, 192000, 768000, -1
+};
+
+static int hdmirx_audio_closest_fs(int fs)
+{
+       int i = 0, fs_t = hdmirx_supported_fs[0];
+
+       while (fs_t > 0) {
+               if (abs(fs - fs_t) <= 2000)
+                       return fs_t;
+               fs_t = hdmirx_supported_fs[++i];
+       }
+       return 0;
+}
+
+/* Recover the incoming audio sample rate from the ACR N/CTS + TMDS clock. */
+static u32 hdmirx_audio_fs(struct snps_hdmirx_dev *hdmirx_dev)
+{
+       u64 tmds_clk, fs_audio = 0;
+       u32 acr_cts, acr_n, tmdsqpclk_freq;
+       u32 acr_pb3_0, acr_pb7_4;
+
+       tmdsqpclk_freq = hdmirx_readl(hdmirx_dev, CMU_TMDSQPCLK_FREQ);
+       hdmirx_readl(hdmirx_dev, PKTDEC_ACR_PH2_1);
+       acr_pb3_0 = hdmirx_readl(hdmirx_dev, PKTDEC_ACR_PB3_0);
+       acr_pb7_4 = hdmirx_readl(hdmirx_dev, PKTDEC_ACR_PB7_4);
+       /*
+        * The packet decoder stores the ACR subpacket bytes with packet byte
+        * 0 in register bits [7:0], so byte-reverse each word to line the
+        * bytes up: CTS is packet bytes 1-3 (PKTDEC_ACR_PB3_0) and N is
+        * packet bytes 4-6 (PKTDEC_ACR_PB7_4), 20 bits each. readl()
+        * already abstracts the bus endianness, so the reversal is
+        * unconditional.
+        */
+       acr_cts = swab32(acr_pb3_0) & 0xfffff;
+       acr_n = (swab32(acr_pb7_4) & 0x0fffff00) >> 8;
+       tmds_clk = tmdsqpclk_freq * 4 * 1000U;
+       if (acr_cts != 0) {
+               fs_audio = div_u64((tmds_clk * acr_n), acr_cts);
+               fs_audio /= 128;
+               fs_audio = hdmirx_audio_closest_fs(fs_audio);
+       }
+       return (u32)fs_audio;
+}
+
+/* Nudge the audio reference clock by +/- ppm to keep the FIFO balanced. */
+static void hdmirx_audio_clk_ppm_inc(struct snps_hdmirx_dev *hdmirx_dev, int 
ppm)
+{
+       int delta, inc;
+       long rate = hdmirx_dev->audio_clkrate;
+
+       if (ppm < 0) {
+               ppm = -ppm;
+               inc = -1;
+       } else {
+               inc = 1;
+       }
+       delta = (int)div64_u64((u64)rate * ppm + 500000, 1000000);
+       delta *= inc;
+       rate = hdmirx_dev->audio_clkrate + delta;
+       clk_set_rate(hdmirx_dev->audio_clk, rate);
+       hdmirx_dev->audio_clkrate = rate;
+}
+
+static int hdmirx_audio_clk_adjust(struct snps_hdmirx_dev *hdmirx_dev,
+                                  int total_offset, int single_offset)
+{
+       int schedule_time = 500;
+       int ppm = 10;
+       u32 offset_abs = abs(total_offset);
+
+       if (offset_abs > 200) {
+               ppm += 200;
+               schedule_time -= 100;
+       }
+       if (offset_abs > 100) {
+               ppm += 200;
+               schedule_time -= 100;
+       }
+       if (offset_abs > 32) {
+               ppm += 20;
+               schedule_time -= 100;
+       }
+       if (offset_abs > 16)
+               ppm += 20;
+       if (total_offset > 16 && single_offset > 0)
+               hdmirx_audio_clk_ppm_inc(hdmirx_dev, ppm);
+       else if (total_offset < -16 && single_offset < 0)
+               hdmirx_audio_clk_ppm_inc(hdmirx_dev, -ppm);
+       return schedule_time;
+}
+
+static void hdmirx_audio_fifo_reinit(struct snps_hdmirx_dev *hdmirx_dev)
+{
+       hdmirx_writel(hdmirx_dev, AUDIO_FIFO_CONTROL, 1);
+       usleep_range(200, 210);
+       hdmirx_writel(hdmirx_dev, AUDIO_FIFO_CONTROL, 0);
+}
+
+/*
+ * Program the audio clock, FIFO thresholds and enables for the given
+ * sample rate. Shared by hw_params and system resume: the controller is
+ * fully reset on resume, so the whole configuration must be re-applied.
+ */
+static void hdmirx_audio_setup(struct snps_hdmirx_dev *hdmirx_dev, u32 fs)
+{
+       hdmirx_dev->audio_fs = fs;
+       hdmirx_dev->audio_clkrate = fs * 128;
+       clk_set_rate(hdmirx_dev->audio_clk, hdmirx_dev->audio_clkrate);
+
+       hdmirx_audio_fifo_reinit(hdmirx_dev);
+       hdmirx_writel(hdmirx_dev, AUDIO_FIFO_THR_PASS, 
HDMIRX_AUDIO_INIT_FIFO_STATE);
+       hdmirx_writel(hdmirx_dev, AUDIO_FIFO_THR,
+                     AFIFO_THR_LOW_QST(0x20) | AFIFO_THR_HIGH_QST(0x160));
+       hdmirx_writel(hdmirx_dev, AUDIO_FIFO_MUTE_THR,
+                     AFIFO_THR_MUTE_LOW_QST(0x8) | 
AFIFO_THR_MUTE_HIGH_QST(0x178));
+
+       hdmirx_update_bits(hdmirx_dev, AUDIO_PROC_CONFIG0, I2S_EN, I2S_EN);
+       hdmirx_update_bits(hdmirx_dev, GLOBAL_SWENABLE, AUDIO_ENABLE, 
AUDIO_ENABLE);
+
+       hdmirx_dev->audio_pre_state = 0;
+}
+
+/*
+ * Periodic worker that locks the local audio clock to the source by keeping
+ * the audio FIFO fill level close to its target, avoiding under/overflow.
+ */
+static void hdmirx_audio_work(struct work_struct *work)
+{
+       struct snps_hdmirx_dev *hdmirx_dev =
+               container_of(to_delayed_work(work), struct snps_hdmirx_dev, 
audio_work);
+       unsigned long delay = 200;
+       int cur, total, single;
+       u32 fifo, fs;
+
+       fs = hdmirx_audio_fs(hdmirx_dev);
+       fifo = hdmirx_readl(hdmirx_dev, AUDIO_FIFO_STATUS2);
+
+       if (fifo & (AFIFO_UNDERFLOW_ST | AFIFO_OVERFLOW_ST)) {
+               if (fs) {
+                       clk_set_rate(hdmirx_dev->audio_clk, fs * 128);
+                       hdmirx_dev->audio_clkrate = fs * 128;
+                       hdmirx_dev->audio_fs = fs;
+               }
+               hdmirx_audio_fifo_reinit(hdmirx_dev);
+               hdmirx_dev->audio_pre_state = 0;
+               goto out;
+       }
+
+       cur = fifo & 0xffff;
+       total = cur - HDMIRX_AUDIO_INIT_STATE;
+       single = cur - hdmirx_dev->audio_pre_state;
+
+       if (fs && abs((int)fs - (int)hdmirx_dev->audio_fs) > 1000) {
+               clk_set_rate(hdmirx_dev->audio_clk, fs * 128);
+               hdmirx_dev->audio_clkrate = fs * 128;
+               hdmirx_dev->audio_fs = fs;
+               hdmirx_audio_fifo_reinit(hdmirx_dev);
+               hdmirx_dev->audio_pre_state = 0;
+               goto out;
+       }
+
+       if (cur != 0)
+               delay = hdmirx_audio_clk_adjust(hdmirx_dev, total, single);
+       hdmirx_dev->audio_pre_state = cur;
+out:
+       /* Only re-arm while streaming; avoids a self-reschedule race with
+        * the cancel_delayed_work_sync() callers (hw_params and
+        * audio_shutdown).
+        */
+       if (READ_ONCE(hdmirx_dev->audio_streaming))
+               queue_delayed_work(system_unbound_wq, &hdmirx_dev->audio_work,
+                                  msecs_to_jiffies(delay));
+}
+
+static int hdmirx_audio_hw_params(struct device *dev, void *data,
+                                 struct hdmi_codec_daifmt *fmt,
+                                 struct hdmi_codec_params *hparms)
+{
+       struct snps_hdmirx_dev *hdmirx_dev = dev_get_drvdata(dev);
+       u32 fs;
+
+       /* Only the I2S interface (DAI 0) is wired up so far. */
+       if (fmt->fmt == HDMI_SPDIF)
+               return -EOPNOTSUPP;
+
+       /*
+        * Stop the worker before touching the shared audio state; it is
+        * re-armed below once the new parameters are in place.
+        */
+       WRITE_ONCE(hdmirx_dev->audio_streaming, false);
+       cancel_delayed_work_sync(&hdmirx_dev->audio_work);
+
+       fs = hdmirx_audio_fs(hdmirx_dev);
+       if (!fs)
+               fs = hparms ? hparms->sample_rate : 48000;
+       if (!fs)
+               fs = 48000;
+
+       hdmirx_audio_setup(hdmirx_dev, fs);
+
+       WRITE_ONCE(hdmirx_dev->audio_streaming, true);
+       mod_delayed_work(system_unbound_wq, &hdmirx_dev->audio_work,
+                        msecs_to_jiffies(200));
+
+       dev_dbg(dev, "audio hw_params: fs=%u\n", fs);
+       return 0;
+}
+
+static void hdmirx_audio_shutdown(struct device *dev, void *data)
+{
+       struct snps_hdmirx_dev *hdmirx_dev = dev_get_drvdata(dev);
+
+       WRITE_ONCE(hdmirx_dev->audio_streaming, false);
+       cancel_delayed_work_sync(&hdmirx_dev->audio_work);
+       hdmirx_update_bits(hdmirx_dev, GLOBAL_SWENABLE, AUDIO_ENABLE, 0);
+}
+
+static const struct hdmi_codec_ops hdmirx_audio_codec_ops = {
+       .hw_params = hdmirx_audio_hw_params,
+       .audio_shutdown = hdmirx_audio_shutdown,
+};
+
+static int hdmirx_register_audio_device(struct snps_hdmirx_dev *hdmirx_dev)
+{
+       struct hdmi_codec_pdata codec_data = {
+               .ops = &hdmirx_audio_codec_ops,
+               .i2s = 1,
+               .no_i2s_playback = 1,
+               .max_i2s_channels = 8,
+               /*
+                * The controller also has an S/PDIF audio interface (DAI 1 in
+                * the binding). Register it so DAI indexes match the binding,
+                * but reject its use in hw_params() until it is wired up.
+                */
+               .spdif = 1,
+               .no_spdif_playback = 1,
+               .data = hdmirx_dev,
+       };
+       struct platform_device *audio_pdev;
+
+       if (!hdmirx_dev->audio_clk)
+               return -ENODEV;
+
+       audio_pdev = platform_device_register_data(hdmirx_dev->dev,
+                                                  HDMI_CODEC_DRV_NAME,
+                                                  PLATFORM_DEVID_AUTO,
+                                                  &codec_data, 
sizeof(codec_data));
+       if (IS_ERR(audio_pdev))
+               return PTR_ERR(audio_pdev);
+
+       hdmirx_dev->audio_pdev = audio_pdev;
+
+       return 0;
+}
+
 static int hdmirx_probe(struct platform_device *pdev)
 {
        struct snps_hdmirx_dev *hdmirx_dev;
@@ -2698,6 +2996,7 @@ static int hdmirx_probe(struct platform_device *pdev)
                          hdmirx_delayed_work_hotplug);
        INIT_DELAYED_WORK(&hdmirx_dev->delayed_work_res_change,
                          hdmirx_delayed_work_res_change);
+       INIT_DELAYED_WORK(&hdmirx_dev->audio_work, hdmirx_audio_work);
 
        hdmirx_dev->cur_fmt_fourcc = V4L2_PIX_FMT_BGR24;
        hdmirx_dev->timings = cea640x480;
@@ -2766,6 +3065,10 @@ static int hdmirx_probe(struct platform_device *pdev)
                                                       V4L2_DEBUGFS_IF_AVI, 
hdmirx_dev,
                                                       hdmirx_debugfs_if_read);
 
+       ret = hdmirx_register_audio_device(hdmirx_dev);
+       if (ret)
+               dev_warn(dev, "failed to register HDMI audio codec: %d\n", ret);
+
        return 0;
 
 err_unreg_video_dev:
@@ -2785,6 +3088,9 @@ static void hdmirx_remove(struct platform_device *pdev)
        struct device *dev = &pdev->dev;
        struct snps_hdmirx_dev *hdmirx_dev = dev_get_drvdata(dev);
 
+       if (hdmirx_dev->audio_pdev)
+               platform_device_unregister(hdmirx_dev->audio_pdev);
+
        v4l2_debugfs_if_free(hdmirx_dev->infoframes);
        debugfs_remove_recursive(hdmirx_dev->debugfs_dir);
 
diff --git a/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.h 
b/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.h
index 31b887e94e87..a99f54fd1744 100644
--- a/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.h
+++ b/drivers/media/platform/synopsys/hdmirx/snps_hdmirx.h
@@ -81,6 +81,7 @@
 #define DATAPATH_ENABLE                                BIT(12)
 #define PKTFIFO_ENABLE                         BIT(11)
 #define AVPUNIT_ENABLE                         BIT(8)
+#define AUDIO_ENABLE                           BIT(9)
 #define MAIN_ENABLE                            BIT(0)
 #define GLOBAL_TIMER_REF_BASE                  0x0028
 #define CORE_CONFIG                            0x0050
@@ -177,20 +178,27 @@
 #define VPROC_FMT_OVR_VALUE(x)                 UPDATE(x, 6, 4)
 #define VPROC_FMT_OVR_EN                       BIT(0)
 
+#define AUDIO_FIFO_CONFIG                      0x0460
 #define AFIFO_FILL_RESTART                     BIT(0)
+#define AUDIO_FIFO_CONTROL                     0x0464
 #define AFIFO_INIT_P                           BIT(0)
+#define AUDIO_FIFO_THR_PASS                    0x0468
+#define AUDIO_FIFO_THR                         0x046c
 #define AFIFO_THR_LOW_QST_MASK                 GENMASK(25, 16)
 #define AFIFO_THR_LOW_QST(x)                   UPDATE(x, 25, 16)
 #define AFIFO_THR_HIGH_QST_MASK                        GENMASK(9, 0)
 #define AFIFO_THR_HIGH_QST(x)                  UPDATE(x, 9, 0)
+#define AUDIO_FIFO_MUTE_THR                    0x0470
 #define AFIFO_THR_MUTE_LOW_QST_MASK            GENMASK(25, 16)
 #define AFIFO_THR_MUTE_LOW_QST(x)              UPDATE(x, 25, 16)
 #define AFIFO_THR_MUTE_HIGH_QST_MASK           GENMASK(9, 0)
 #define AFIFO_THR_MUTE_HIGH_QST(x)             UPDATE(x, 9, 0)
 
+#define AUDIO_FIFO_STATUS2                     0x0478
 #define AFIFO_UNDERFLOW_ST                     BIT(25)
 #define AFIFO_OVERFLOW_ST                      BIT(24)
 
+#define AUDIO_PROC_CONFIG0                     0x0480
 #define SPEAKER_ALLOC_OVR_EN                   BIT(16)
 #define I2S_BPCUV_EN                           BIT(4)
 #define SPDIF_EN                               BIT(2)
_______________________________________________
linuxtv-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to