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]