git: 59fccd1e84e7 - stable/14 - Setups with digital audio connections like SPDIF and ADAT require a designated master clock to stay in sync. Add a sysctl setting to control the preferred clock source for each HDSPe sound card. Complement this by sysctl values to list available clock sources, show the currently effective clock source and display the sync status of all connections. Clock sources are named according to RME user manuals.

From: Christos Margiolis <christos_at_FreeBSD.org>
Date: Tue, 30 Jan 2024 11:08:21 UTC
The branch stable/14 has been updated by christos:

URL: https://cgit.FreeBSD.org/src/commit/?id=59fccd1e84e7ca9132a31d7f7ce028359eef1733

commit 59fccd1e84e7ca9132a31d7f7ce028359eef1733
Author:     Ruslan Bukin <br@FreeBSD.org>
AuthorDate: 2024-01-04 09:35:00 +0000
Commit:     Christos Margiolis <christos@FreeBSD.org>
CommitDate: 2024-01-30 11:07:28 +0000

    Setups with digital audio connections like SPDIF and ADAT require
    a designated master clock to stay in sync. Add a sysctl setting
    to control the preferred clock source for each HDSPe sound card.
    Complement this by sysctl values to list available clock sources,
    show the currently effective clock source and display the sync
    status of all connections. Clock sources are named according to
    RME user manuals.
    
    Submitted by:   Florian Walpen <dev@submerge.ch>
    Differential Revision:  https://reviews.freebsd.org/D43252
    
    (cherry picked from commit b6052c10fb4ad70915e2f82ce606fd7715477eef)
---
 share/man/man4/snd_hdspe.4    |  41 +++++++-
 sys/dev/sound/pci/hdspe-pcm.c |   4 +-
 sys/dev/sound/pci/hdspe.c     | 226 ++++++++++++++++++++++++++++++++++++++++--
 sys/dev/sound/pci/hdspe.h     |  27 +++--
 4 files changed, 282 insertions(+), 16 deletions(-)

diff --git a/share/man/man4/snd_hdspe.4 b/share/man/man4/snd_hdspe.4
index dba81f653d4b..4b925b14aef6 100644
--- a/share/man/man4/snd_hdspe.4
+++ b/share/man/man4/snd_hdspe.4
@@ -22,7 +22,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd February 13, 2012
+.Dd December 30, 2023
 .Dt SND_HDSPE 4
 .Os
 .Sh NAME
@@ -59,6 +59,45 @@ RME HDSPe AIO
 .It
 RME HDSPe RayDAT
 .El
+.Sh SYSCTL TUNABLES
+These settings and informational values can be accessed at runtime with the
+.Xr sysctl 8
+command.
+If multiple RME HDSPe sound cards are installed, each device has a separate
+configuration.
+To adjust the following sysctl identifiers for a specific sound card, insert
+the respective device number in place of
+.Ql 0 .
+.Bl -tag -width indent
+.It Va dev.hdspe.0.clock_list
+Lists possible clock sources to sync with, depending on the hardware model.
+This includes internal and external master clocks as well as incoming digital
+audio signals like AES, S/PDIF and ADAT.
+.It Va dev.hdspe.0.clock_preference
+Select a preferred clock source from the clock list.
+HDSPe cards will sync to this clock source when available, but fall back to
+auto-sync with any other digital clock signal they receive.
+Set this to
+.Ql internal
+if the HDSPe card should act as master clock.
+.It Va dev.hdspe.0.clock_source
+Shows the actual clock source in use (read only).
+This differs from what is set as clock preference when in auto-sync mode.
+.It Va dev.hdspe.0.sync_status
+Display the current sync status of all external clock sources.
+Status indications are
+.Ql none
+for no signal at all,
+.Ql lock
+for when a valid signal is present, and
+.Ql sync
+for accurately synchronized signals (required for recording digital
+audio).
+.El
+.Pp
+Where appropriate these sysctl values are modeled after official RME software on
+other platforms, and adopt their terminology.
+Consult the RME user manuals for additional information.
 .Sh SEE ALSO
 .Xr sound 4
 .Sh HISTORY
diff --git a/sys/dev/sound/pci/hdspe-pcm.c b/sys/dev/sound/pci/hdspe-pcm.c
index 04ab7d2d35ce..b3daed4d9599 100644
--- a/sys/dev/sound/pci/hdspe-pcm.c
+++ b/sys/dev/sound/pci/hdspe-pcm.c
@@ -519,8 +519,8 @@ hdspechan_setspeed(kobj_t obj, void *data, uint32_t speed)
 	}
 
 	switch (sc->type) {
-	case RAYDAT:
-	case AIO:
+	case HDSPE_RAYDAT:
+	case HDSPE_AIO:
 		period = HDSPE_FREQ_AIO;
 		break;
 	default:
diff --git a/sys/dev/sound/pci/hdspe.c b/sys/dev/sound/pci/hdspe.c
index 7124fb1f5c57..8a7cac87fc1a 100644
--- a/sys/dev/sound/pci/hdspe.c
+++ b/sys/dev/sound/pci/hdspe.c
@@ -31,6 +31,9 @@
  * Supported cards: AIO, RayDAT.
  */
 
+#include <sys/types.h>
+#include <sys/sysctl.h>
+
 #include <dev/sound/pcm/sound.h>
 #include <dev/sound/pci/hdspe.h>
 #include <dev/sound/chip.h>
@@ -40,6 +43,31 @@
 
 #include <mixer_if.h>
 
+static struct hdspe_clock_source hdspe_clock_source_table_rd[] = {
+	{ "internal", 0 << 1 | 1, HDSPE_STATUS1_CLOCK(15),       0,       0 },
+	{ "word",     0 << 1 | 0, HDSPE_STATUS1_CLOCK( 0), 1 << 24, 1 << 25 },
+	{ "aes",      1 << 1 | 0, HDSPE_STATUS1_CLOCK( 1),  1 << 0,  1 << 8 },
+	{ "spdif",    2 << 1 | 0, HDSPE_STATUS1_CLOCK( 2),  1 << 1,  1 << 9 },
+	{ "adat1",    3 << 1 | 0, HDSPE_STATUS1_CLOCK( 3),  1 << 2, 1 << 10 },
+	{ "adat2",    4 << 1 | 0, HDSPE_STATUS1_CLOCK( 4),  1 << 3, 1 << 11 },
+	{ "adat3",    5 << 1 | 0, HDSPE_STATUS1_CLOCK( 5),  1 << 4, 1 << 12 },
+	{ "adat4",    6 << 1 | 0, HDSPE_STATUS1_CLOCK( 6),  1 << 5, 1 << 13 },
+	{ "tco",      9 << 1 | 0, HDSPE_STATUS1_CLOCK( 9), 1 << 26, 1 << 27 },
+	{ "sync_in", 10 << 1 | 0, HDSPE_STATUS1_CLOCK(10),       0,       0 },
+	{ NULL,       0 << 1 | 0, HDSPE_STATUS1_CLOCK( 0),       0,       0 },
+};
+
+static struct hdspe_clock_source hdspe_clock_source_table_aio[] = {
+	{ "internal", 0 << 1 | 1, HDSPE_STATUS1_CLOCK(15),       0,       0 },
+	{ "word",     0 << 1 | 0, HDSPE_STATUS1_CLOCK( 0), 1 << 24, 1 << 25 },
+	{ "aes",      1 << 1 | 0, HDSPE_STATUS1_CLOCK( 1),  1 << 0,  1 << 8 },
+	{ "spdif",    2 << 1 | 0, HDSPE_STATUS1_CLOCK( 2),  1 << 1,  1 << 9 },
+	{ "adat",     3 << 1 | 0, HDSPE_STATUS1_CLOCK( 3),  1 << 2, 1 << 10 },
+	{ "tco",      9 << 1 | 0, HDSPE_STATUS1_CLOCK( 9), 1 << 26, 1 << 27 },
+	{ "sync_in", 10 << 1 | 0, HDSPE_STATUS1_CLOCK(10),       0,       0 },
+	{ NULL,       0 << 1 | 0, HDSPE_STATUS1_CLOCK( 0),       0,       0 },
+};
+
 static struct hdspe_channel chan_map_aio[] = {
 	{  0,  1,   "line", 1, 1 },
 	{  6,  7,  "phone", 1, 0 },
@@ -224,6 +252,169 @@ hdspe_map_dmabuf(struct sc_info *sc)
 	}
 }
 
+static int
+hdspe_sysctl_clock_preference(SYSCTL_HANDLER_ARGS)
+{
+	struct sc_info *sc;
+	struct hdspe_clock_source *clock_table, *clock;
+	char buf[16] = "invalid";
+	int error;
+	uint32_t setting;
+
+	sc = oidp->oid_arg1;
+
+	/* Select sync ports table for device type. */
+	if (sc->type == HDSPE_AIO)
+		clock_table = hdspe_clock_source_table_aio;
+	else if (sc->type == HDSPE_RAYDAT)
+		clock_table = hdspe_clock_source_table_rd;
+	else
+		return (ENXIO);
+
+	/* Extract preferred clock source from settings register. */
+	setting = sc->settings_register & HDSPE_SETTING_CLOCK_MASK;
+	for (clock = clock_table; clock->name != NULL; ++clock) {
+		if (clock->setting == setting)
+			break;
+	}
+	if (clock->name != NULL)
+		strlcpy(buf, clock->name, sizeof(buf));
+
+	/* Process sysctl string request. */
+	error = sysctl_handle_string(oidp, buf, sizeof(buf), req);
+	if (error != 0 || req->newptr == NULL)
+		return (error);
+
+	/* Find clock source matching the sysctl string. */
+	for (clock = clock_table; clock->name != NULL; ++clock) {
+		if (strncasecmp(buf, clock->name, sizeof(buf)) == 0)
+			break;
+	}
+
+	/* Set preferred clock source in settings register. */
+	if (clock->name != NULL) {
+		setting = clock->setting & HDSPE_SETTING_CLOCK_MASK;
+		snd_mtxlock(sc->lock);
+		sc->settings_register &= ~HDSPE_SETTING_CLOCK_MASK;
+		sc->settings_register |= setting;
+		hdspe_write_4(sc, HDSPE_SETTINGS_REG, sc->settings_register);
+		snd_mtxunlock(sc->lock);
+	}
+	return (0);
+}
+
+static int
+hdspe_sysctl_clock_source(SYSCTL_HANDLER_ARGS)
+{
+	struct sc_info *sc;
+	struct hdspe_clock_source *clock_table, *clock;
+	char buf[16] = "invalid";
+	uint32_t status;
+
+	sc = oidp->oid_arg1;
+
+	/* Select sync ports table for device type. */
+	if (sc->type == HDSPE_AIO)
+		clock_table = hdspe_clock_source_table_aio;
+	else if (sc->type == HDSPE_RAYDAT)
+		clock_table = hdspe_clock_source_table_rd;
+	else
+		return (ENXIO);
+
+	/* Read current (autosync) clock source from status register. */
+	snd_mtxlock(sc->lock);
+	status = hdspe_read_4(sc, HDSPE_STATUS1_REG);
+	status &= HDSPE_STATUS1_CLOCK_MASK;
+	snd_mtxunlock(sc->lock);
+
+	/* Translate status register value to clock source. */
+	for (clock = clock_table; clock->name != NULL; ++clock) {
+		/* In clock master mode, override with internal clock source. */
+		if (sc->settings_register & HDSPE_SETTING_MASTER) {
+			if (clock->setting & HDSPE_SETTING_MASTER)
+				break;
+		} else if (clock->status == status)
+			break;
+	}
+
+	/* Process sysctl string request. */
+	if (clock->name != NULL)
+		strlcpy(buf, clock->name, sizeof(buf));
+	return (sysctl_handle_string(oidp, buf, sizeof(buf), req));
+}
+
+static int
+hdspe_sysctl_clock_list(SYSCTL_HANDLER_ARGS)
+{
+	struct sc_info *sc;
+	struct hdspe_clock_source *clock_table, *clock;
+	char buf[256];
+	int n;
+
+	sc = oidp->oid_arg1;
+	n = 0;
+
+	/* Select clock source table for device type. */
+	if (sc->type == HDSPE_AIO)
+		clock_table = hdspe_clock_source_table_aio;
+	else if (sc->type == HDSPE_RAYDAT)
+		clock_table = hdspe_clock_source_table_rd;
+	else
+		return (ENXIO);
+
+	/* List available clock sources. */
+	buf[0] = 0;
+	for (clock = clock_table; clock->name != NULL; ++clock) {
+		if (n > 0)
+			n += strlcpy(buf + n, ",", sizeof(buf) - n);
+		n += strlcpy(buf + n, clock->name, sizeof(buf) - n);
+	}
+	return (sysctl_handle_string(oidp, buf, sizeof(buf), req));
+}
+
+static int
+hdspe_sysctl_sync_status(SYSCTL_HANDLER_ARGS)
+{
+	struct sc_info *sc;
+	struct hdspe_clock_source *clock_table, *clock;
+	char buf[256];
+	char *state;
+	int n;
+	uint32_t status;
+
+	sc = oidp->oid_arg1;
+	n = 0;
+
+	/* Select sync ports table for device type. */
+	if (sc->type == HDSPE_AIO)
+		clock_table = hdspe_clock_source_table_aio;
+	else if (sc->type == HDSPE_RAYDAT)
+		clock_table = hdspe_clock_source_table_rd;
+	else
+		return (ENXIO);
+
+	/* Read current lock and sync bits from status register. */
+	snd_mtxlock(sc->lock);
+	status = hdspe_read_4(sc, HDSPE_STATUS1_REG);
+	snd_mtxunlock(sc->lock);
+
+	/* List clock sources with lock and sync state. */
+	for (clock = clock_table; clock->name != NULL; ++clock) {
+		if (clock->sync_bit != 0) {
+			if (n > 0)
+				n += strlcpy(buf + n, ",", sizeof(buf) - n);
+			state = "none";
+			if ((clock->sync_bit & status) != 0)
+				state = "sync";
+			else if ((clock->lock_bit & status) != 0)
+				state = "lock";
+			n += snprintf(buf + n, sizeof(buf) - n, "%s(%s)",
+			    clock->name, state);
+		}
+	}
+	return (sysctl_handle_string(oidp, buf, sizeof(buf), req));
+}
+
 static int
 hdspe_probe(device_t dev)
 {
@@ -250,9 +441,6 @@ hdspe_init(struct sc_info *sc)
 {
 	long long period;
 
-	/* Set defaults. */
-	sc->ctrl_register |= HDSPM_CLOCK_MODE_MASTER;
-
 	/* Set latency. */
 	sc->period = 32;
 	sc->ctrl_register = hdspe_encode_latency(7);
@@ -264,8 +452,8 @@ hdspe_init(struct sc_info *sc)
 	hdspe_write_4(sc, HDSPE_CONTROL_REG, sc->ctrl_register);
 
 	switch (sc->type) {
-	case RAYDAT:
-	case AIO:
+	case HDSPE_RAYDAT:
+	case HDSPE_AIO:
 		period = HDSPE_FREQ_AIO;
 		break;
 	default:
@@ -305,11 +493,11 @@ hdspe_attach(device_t dev)
 	rev = pci_get_revid(dev);
 	switch (rev) {
 	case PCI_REVISION_AIO:
-		sc->type = AIO;
+		sc->type = HDSPE_AIO;
 		chan_map = chan_map_aio;
 		break;
 	case PCI_REVISION_RAYDAT:
-		sc->type = RAYDAT;
+		sc->type = HDSPE_RAYDAT;
 		chan_map = chan_map_rd;
 		break;
 	default:
@@ -336,6 +524,30 @@ hdspe_attach(device_t dev)
 
 	hdspe_map_dmabuf(sc);
 
+	SYSCTL_ADD_PROC(device_get_sysctl_ctx(dev),
+	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+	    "sync_status", CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_MPSAFE,
+	    sc, 0, hdspe_sysctl_sync_status, "A",
+	    "List clock source signal lock and sync status");
+
+	SYSCTL_ADD_PROC(device_get_sysctl_ctx(dev),
+	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+	    "clock_source", CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_MPSAFE,
+	    sc, 0, hdspe_sysctl_clock_source, "A",
+	    "Currently effective clock source");
+
+	SYSCTL_ADD_PROC(device_get_sysctl_ctx(dev),
+	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+	    "clock_preference", CTLTYPE_STRING | CTLFLAG_RW | CTLFLAG_MPSAFE,
+	    sc, 0, hdspe_sysctl_clock_preference, "A",
+	    "Set 'internal' (master) or preferred autosync clock source");
+
+	SYSCTL_ADD_PROC(device_get_sysctl_ctx(dev),
+	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+	    "clock_list", CTLTYPE_STRING | CTLFLAG_RD | CTLFLAG_MPSAFE,
+	    sc, 0, hdspe_sysctl_clock_list, "A",
+	    "List of supported clock sources");
+
 	return (bus_generic_attach(dev));
 }
 
diff --git a/sys/dev/sound/pci/hdspe.h b/sys/dev/sound/pci/hdspe.h
index 7056a75a66c1..58362641cb01 100644
--- a/sys/dev/sound/pci/hdspe.h
+++ b/sys/dev/sound/pci/hdspe.h
@@ -32,8 +32,8 @@
 #define	PCI_REVISION_AIO		212
 #define	PCI_REVISION_RAYDAT		211
 
-#define	AIO				0
-#define	RAYDAT				1
+#define	HDSPE_AIO			0
+#define	HDSPE_RAYDAT			1
 
 /* Hardware mixer */
 #define	HDSPE_OUT_ENABLE_BASE		512
@@ -95,15 +95,13 @@
 #define	HDSP_PhoneGainMinus6dB		(HDSP_PhoneGain0)
 #define	HDSP_PhoneGainMinus12dB		0
 
-#define	HDSPM_statusRegister		0
-#define	HDSPM_statusRegister2		192
-
 /* Settings */
 #define	HDSPE_SETTINGS_REG		0
 #define	HDSPE_CONTROL_REG		64
 #define	HDSPE_STATUS_REG		0
+#define	HDSPE_STATUS1_REG		64
+#define	HDSPE_STATUS2_REG		192
 #define	HDSPE_ENABLE			(1 << 0)
-#define	HDSPM_CLOCK_MODE_MASTER		(1 << 4)
 
 /* Interrupts */
 #define	HDSPE_AUDIO_IRQ_PENDING		(1 << 0)
@@ -126,6 +124,23 @@ struct hdspe_channel {
 	uint32_t	rec;
 };
 
+/* Clock sources */
+#define	HDSPE_SETTING_MASTER		(1 << 0)
+#define	HDSPE_SETTING_CLOCK_MASK	0x1f
+
+#define	HDSPE_STATUS1_CLOCK_SHIFT	28
+#define	HDSPE_STATUS1_CLOCK_MASK	(0x0f << HDSPE_STATUS1_CLOCK_SHIFT)
+#define	HDSPE_STATUS1_CLOCK(n)		(((n) << HDSPE_STATUS1_CLOCK_SHIFT) & \
+					HDSPE_STATUS1_CLOCK_MASK)
+
+struct hdspe_clock_source {
+	char		*name;
+	uint32_t	setting;
+	uint32_t	status;
+	uint32_t	lock_bit;
+	uint32_t	sync_bit;
+};
+
 static MALLOC_DEFINE(M_HDSPE, "hdspe", "hdspe audio");
 
 /* Channel registers */