[PATCH 18/20] net: dsa: xilinx: offload the bridge FDB to the switch CAM

Nagadheeraj Rottela <[email protected]>
Newsgroups org.kernel.vger.netdev,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
The switch resolves forwarding through a stream-destination lookup
CAM that maps a (destination MAC, VLAN ID) key to an egress port
list. Back the bridge FDB with it.

The CAM is an indirect register block. Load the key and port-list
registers, write the opcode (add, delete, read, or read-key) to the
control register, and wait for the operation to finish. Add a mutex
to serialise these sequences.

The fabric also learns source MACs into the same CAM. Enable the
learning engine in port_bridge_join, and add port_bridge_leave to
disable it once the last user port leaves, so entries accumulate
only while a bridge spans the ports.

port_fdb_add and port_fdb_del read the entry, set or clear the
calling port's bit in the port list, and write it back. Delete the
entry once no port references it. A (MAC, VID) reachable on several
ports then carries a correct multi-port list. The DSA "no VLAN" key
(vid 0) maps to the default native VID that an untagged frame uses.

port_fdb_dump reports a port's learnt entries. The hardware scatters
them across the port's read-key region. Scan all of it and report
every slot that is present. The CPU port has no such region.

port_fast_age flushes a port's learnt addresses from the CAM.

Signed-off-by: Nagadheeraj Rottela <[email protected]>
---
 drivers/net/dsa/xilinx/xilinx_tsn.c | 301 +++++++++++++++++++++++++++-
 drivers/net/dsa/xilinx/xilinx_tsn.h |  93 ++++++++-
 2 files changed, 391 insertions(+), 3 deletions(-)

diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.c b/drivers/net/dsa/xilinx/xilinx_tsn.c
index 9a4d4a2fb892..57558b0e2613 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.c
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.c
@@ -174,6 +174,31 @@ static int xlnx_tsn_set_port_state(struct xlnx_tsn *sw, int port,
 	return 0;
 }
 
+static enum tsn_port_state xlnx_tsn_get_port_state(struct xlnx_tsn *sw,
+						   int port)
+{
+	u32 mask, chg_bit, reg;
+
+	if (xlnx_tsn_port_state_bits(port, &mask, &chg_bit))
+		return TSN_PORT_STATE_DISABLED;
+
+	reg = sw_ior(sw, TSN_PORT_STATE_CTRL_OFFSET);
+	return (reg & mask) >> __ffs(mask);
+}
+
+static int xlnx_tsn_port_state_cycle(struct xlnx_tsn *sw, int port,
+				     enum tsn_port_state state)
+{
+	enum tsn_port_state saved = xlnx_tsn_get_port_state(sw, port);
+	int err;
+
+	err = xlnx_tsn_set_port_state(sw, port, state);
+	if (err)
+		return err;
+
+	return xlnx_tsn_set_port_state(sw, port, saved);
+}
+
 static int xlnx_tsn_mdio_wait_ready(struct xlnx_tsn_mac *m)
 {
 	u32 val;
@@ -495,11 +520,32 @@ static int xlnx_tsn_port_set_mac_address(struct dsa_switch *ds, int port,
 	return 0;
 }
 
+/* Enable or disable the hardware address learning engine globally.
+ * Standalone ports do not learn. Enable when a port joins a bridge,
+ * disable when the last one leaves. The sub-qualifier bits below are
+ * inert while learning is disabled. Caller holds indirect_lock.
+ */
+static void xlnx_tsn_set_global_learning(struct xlnx_tsn *sw, bool on)
+{
+	u32 reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+
+	if (on)
+		reg &= ~TSN_SW_ADDR_LEARN_DISABLE;
+	else
+		reg |= TSN_SW_ADDR_LEARN_DISABLE;
+	sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+}
+
 static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
 				     struct dsa_bridge bridge,
 				     bool *tx_fwd_offload,
 				     struct netlink_ext_ack *extack)
 {
+	struct xlnx_tsn *sw = ds->priv;
+
+	scoped_guard(mutex, &sw->indirect_lock)
+		xlnx_tsn_set_global_learning(sw, true);
+
 	/* The switch fabric replicates flooded frames per egress port
 	 * on its own, so the bridge does not need to clone-and-send.
 	 */
@@ -508,6 +554,24 @@ static int xlnx_tsn_port_bridge_join(struct dsa_switch *ds, int port,
 	return 0;
 }
 
+static void xlnx_tsn_port_bridge_leave(struct dsa_switch *ds, int port,
+				       struct dsa_bridge bridge)
+{
+	struct xlnx_tsn *sw = ds->priv;
+	struct dsa_port *dp;
+
+	dsa_switch_for_each_user_port(dp, ds) {
+		if (dp->index == port)
+			continue;
+
+		if (dp->bridge)
+			return;
+	}
+
+	scoped_guard(mutex, &sw->indirect_lock)
+		xlnx_tsn_set_global_learning(sw, false);
+}
+
 static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
 					u8 state)
 {
@@ -539,6 +603,217 @@ static void xlnx_tsn_port_stp_state_set(struct dsa_switch *ds, int port,
 	xlnx_tsn_set_port_state(sw, port, hw_state);
 }
 
+/* The CAM status enable bit reads 1 when the block is ready to accept the
+ * next operation.
+ */
+static int xlnx_tsn_cam_wait_ready(struct xlnx_tsn *sw)
+{
+	u32 reg;
+
+	return readl_poll_timeout(sw->sw_base + TSN_CAM_STATUS_OFFSET, reg,
+				  reg & TSN_CAM_STATUS_READY, TSN_SW_POLL_DELAY_US,
+				  TSN_SW_POLL_TIMEOUT_US);
+}
+
+/* The CAM control enable bit self-clears when the operation completes. */
+static int xlnx_tsn_cam_wait_done(struct xlnx_tsn *sw)
+{
+	u32 reg;
+
+	return readl_poll_timeout(sw->sw_base + TSN_CAM_CTRL_OFFSET, reg,
+				  !(reg & TSN_CAM_OP_ENABLE),
+				  TSN_SW_POLL_DELAY_US, TSN_SW_POLL_TIMEOUT_US);
+}
+
+static void xlnx_tsn_cam_load_key(struct xlnx_tsn *sw,
+				  const unsigned char *addr, u16 vid)
+{
+	sw_iow(sw, TSN_CAM_KEY1_OFFSET,
+	       ((u32)addr[0] << 24) | ((u32)addr[1] << 16) |
+	       ((u32)addr[2] << 8) | addr[3]);
+	sw_iow(sw, TSN_CAM_KEY2_OFFSET,
+	       ((u32)addr[4] << 8) | addr[5] |
+	       FIELD_PREP(TSN_CAM_VLAN, vid));
+}
+
+/* Look up a (MAC, VID) key and return its current egress port list, or 0
+ * if the entry is absent. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_cam_read_portlist(struct xlnx_tsn *sw,
+				      const unsigned char *addr, u16 vid,
+				      u8 *portlist)
+{
+	u32 ctrl;
+	int ret;
+
+	ret = xlnx_tsn_cam_wait_ready(sw);
+	if (ret)
+		return ret;
+
+	xlnx_tsn_cam_load_key(sw, addr, vid);
+	sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+	       FIELD_PREP(TSN_CAM_OP_MASK, TSN_CAM_OP_READ) | TSN_CAM_OP_ENABLE);
+
+	ret = xlnx_tsn_cam_wait_done(sw);
+	if (ret)
+		return ret;
+
+	ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
+	if (!(ctrl & TSN_CAM_FOUND)) {
+		*portlist = 0;
+		return 0;
+	}
+
+	*portlist = FIELD_GET(TSN_CAM_PORT_LIST,
+			      sw_ior(sw, TSN_CAM_PORT_ACT_OFFSET));
+	return 0;
+}
+
+/* Add (add=true) or delete (add=false) the (MAC, VID) entry carrying the
+ * given port list. Caller holds indirect_lock.
+ */
+static int xlnx_tsn_cam_write(struct xlnx_tsn *sw, const unsigned char *addr,
+			      u16 vid, u8 portlist, bool add)
+{
+	int ret;
+
+	ret = xlnx_tsn_cam_wait_ready(sw);
+	if (ret)
+		return ret;
+
+	xlnx_tsn_cam_load_key(sw, addr, vid);
+	sw_iow(sw, TSN_CAM_TV1_OFFSET, 0);
+	sw_iow(sw, TSN_CAM_TV2_OFFSET, 0);
+	sw_iow(sw, TSN_CAM_PORT_ACT_OFFSET,
+	       FIELD_PREP(TSN_CAM_PORT_LIST, portlist));
+	sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+	       FIELD_PREP(TSN_CAM_OP_MASK, add ? TSN_CAM_OP_ADD : TSN_CAM_OP_DELETE) |
+	       TSN_CAM_OP_ENABLE);
+
+	return xlnx_tsn_cam_wait_done(sw);
+}
+
+static void xlnx_tsn_port_fast_age(struct dsa_switch *ds, int port)
+{
+	struct xlnx_tsn *sw = ds->priv;
+	int err;
+
+	err = xlnx_tsn_port_state_cycle(sw, port, TSN_PORT_STATE_FLUSH);
+	if (err)
+		dev_err(sw->dev, "port %d: fast age failed (%d)\n", port, err);
+}
+
+static int xlnx_tsn_port_fdb_add(struct dsa_switch *ds, int port,
+				 const unsigned char *addr, u16 vid,
+				 struct dsa_db db)
+{
+	struct xlnx_tsn *sw = ds->priv;
+	u8 portlist;
+	int ret;
+
+	if (!vid)
+		vid = TSN_SW_DEFAULT_VID;
+
+	guard(mutex)(&sw->indirect_lock);
+	ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
+	if (!ret) {
+		portlist |= TSN_PORT_BIT(port);
+		ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, true);
+	}
+
+	return ret;
+}
+
+static int xlnx_tsn_port_fdb_del(struct dsa_switch *ds, int port,
+				 const unsigned char *addr, u16 vid,
+				 struct dsa_db db)
+{
+	struct xlnx_tsn *sw = ds->priv;
+	u8 portlist;
+	int ret;
+
+	if (!vid)
+		vid = TSN_SW_DEFAULT_VID;
+
+	guard(mutex)(&sw->indirect_lock);
+	ret = xlnx_tsn_cam_read_portlist(sw, addr, vid, &portlist);
+	if (!ret) {
+		if (!portlist)
+			return 0;
+
+		/* Drop the entry once no port references it. Otherwise
+		 * rewrite it with the updated port list.
+		 */
+		portlist &= ~TSN_PORT_BIT(port);
+		ret = xlnx_tsn_cam_write(sw, addr, vid, portlist, portlist != 0);
+	}
+
+	return ret;
+}
+
+static int xlnx_tsn_port_fdb_dump(struct dsa_switch *ds, int port,
+				  dsa_fdb_dump_cb_t *cb, void *data)
+{
+	struct xlnx_tsn *sw = ds->priv;
+	unsigned char addr[ETH_ALEN];
+	u32 base, ctrl, key1, key2;
+	int ret = 0;
+	u16 vid;
+	u32 i;
+
+	/* Learnt entries live in a per-MAC-port read-key region. The CPU
+	 * port has no such region.
+	 */
+	if (port == XLNX_TSN_CPU_PORT)
+		return 0;
+
+	guard(mutex)(&sw->indirect_lock);
+
+	if (port == XLNX_TSN_PORT_MAC2)
+		base = TSN_CAM_MAC2_READ_KEY_BASE;
+	else
+		base = 0;
+
+	/* Learnt entries occupy non-consecutive slots, so scan the whole
+	 * region and report each slot marked found.
+	 */
+	for (i = 0; i < TSN_CAM_READ_KEY_COUNT; i++) {
+		ret = xlnx_tsn_cam_wait_ready(sw);
+		if (ret)
+			return ret;
+
+		sw_iow(sw, TSN_CAM_CTRL_OFFSET,
+		       FIELD_PREP(TSN_CAM_READ_KEY_ADDR, base + i) |
+		       FIELD_PREP(TSN_CAM_OP_MASK, TSN_CAM_OP_READ_KEY) |
+		       TSN_CAM_OP_ENABLE);
+
+		ret = xlnx_tsn_cam_wait_done(sw);
+		if (ret)
+			return ret;
+
+		ctrl = sw_ior(sw, TSN_CAM_CTRL_OFFSET);
+		if (!(ctrl & TSN_CAM_FOUND))
+			continue;
+
+		key1 = sw_ior(sw, TSN_CAM_KEY1_OFFSET);
+		key2 = sw_ior(sw, TSN_CAM_KEY2_OFFSET);
+		addr[0] = key1 >> 24;
+		addr[1] = key1 >> 16;
+		addr[2] = key1 >> 8;
+		addr[3] = key1;
+		addr[4] = key2 >> 8;
+		addr[5] = key2;
+		vid = FIELD_GET(TSN_CAM_VLAN, key2);
+		if (vid == TSN_SW_DEFAULT_VID)
+			vid = 0;
+
+		ret = cb(addr, vid, false, data);
+		if (ret)
+			return ret;
+	}
+	return ret;
+}
+
 static void xlnx_tsn_phylink_get_caps(struct dsa_switch *ds, int port,
 				      struct phylink_config *config)
 {
@@ -643,7 +918,7 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
 	struct dsa_port *cpu_dp = dsa_to_port(ds, XLNX_TSN_CPU_PORT);
 	struct xlnx_tsn *sw = ds->priv;
 	struct dsa_port *dp;
-	u32 mgmt;
+	u32 mgmt, reg;
 	int ret;
 
 	if (!dsa_is_user_port(ds, XLNX_TSN_PORT_MAC1) ||
@@ -656,6 +931,22 @@ static int xlnx_tsn_setup(struct dsa_switch *ds)
 
 	sw->conduit = cpu_dp->conduit;
 
+	/* Pre-arm the learning sub-qualifiers for when a port joins a bridge:
+	 * learn untagged frames under their ingress native VID, and allow
+	 * learning on VIDs with no membership entry while the bridge is
+	 * VLAN-unaware. Both bits are inert while global learning is disabled.
+	 */
+	reg = sw_ior(sw, TSN_SW_ADDR_LEARN_OFFSET);
+	reg |= TSN_SW_ADDR_LEARN_DISABLE | TSN_SW_ADDR_LEARN_UNTAGGED_EN |
+	       TSN_SW_ADDR_LEARN_NO_VLAN_EN;
+	sw_iow(sw, TSN_SW_ADDR_LEARN_OFFSET, reg);
+
+	/* On a CAM miss flood unknown tagged unicast frames to all ports. */
+	reg = sw_ior(sw, TSN_SW_CTRL_OFFSET);
+	reg &= ~TSN_SW_CTRL_UCAST_MISS_MASK;
+	reg |= FIELD_PREP(TSN_SW_CTRL_UCAST_MISS_MASK, TSN_SW_CTRL_UCAST_MISS_FLOOD);
+	sw_iow(sw, TSN_SW_CTRL_OFFSET, reg);
+
 	/* Route CPU-originated bridge-group control frames (STP, LLDP) to
 	 * the single wire port whose MAC-nibble field matches the frame's
 	 * source-MAC low nibble, instead of flooding to both.
@@ -743,7 +1034,12 @@ static const struct dsa_switch_ops xlnx_tsn_switch_ops = {
 	.teardown		= xlnx_tsn_teardown,
 	.port_set_mac_address	= xlnx_tsn_port_set_mac_address,
 	.port_bridge_join	= xlnx_tsn_port_bridge_join,
+	.port_bridge_leave	= xlnx_tsn_port_bridge_leave,
 	.port_stp_state_set	= xlnx_tsn_port_stp_state_set,
+	.port_fdb_add		= xlnx_tsn_port_fdb_add,
+	.port_fdb_del		= xlnx_tsn_port_fdb_del,
+	.port_fdb_dump		= xlnx_tsn_port_fdb_dump,
+	.port_fast_age		= xlnx_tsn_port_fast_age,
 	.port_hwtstamp_get	= xlnx_tsn_port_hwtstamp_get,
 	.port_hwtstamp_set	= xlnx_tsn_port_hwtstamp_set,
 	.get_ts_info		= xlnx_tsn_get_ts_info,
@@ -778,6 +1074,9 @@ static int xlnx_tsn_probe(struct platform_device *pdev)
 	sw->dev = dev;
 	sw->mac[XLNX_TSN_PORT_MAC1].sw = sw;
 	sw->mac[XLNX_TSN_PORT_MAC2].sw = sw;
+	ret = devm_mutex_init(dev, &sw->indirect_lock);
+	if (ret)
+		return ret;
 
 	ret = xlnx_tsn_map_reg(pdev, "switch", &sw->sw_base);
 	if (ret)
diff --git a/drivers/net/dsa/xilinx/xilinx_tsn.h b/drivers/net/dsa/xilinx/xilinx_tsn.h
index d46150535775..a94f5124a33d 100644
--- a/drivers/net/dsa/xilinx/xilinx_tsn.h
+++ b/drivers/net/dsa/xilinx/xilinx_tsn.h
@@ -63,18 +63,104 @@
 #define TSN_SW_MGMT_QUEUING_OFFSET		0x00054
 #define TSN_SW_MGMT_QUEUING_EP_SA_EGRESS	BIT(4)
 
-/* readl_poll_timeout() parameters (in microseconds): poll until
- * the port-state change-commit bit self-clears.
+/* Shared readl_poll_timeout() parameters (in microseconds) used at
+ * two call sites: the port-state change-commit bit and the CAM
+ * operation enable bit. Both self-clear when the operation completes.
  */
 #define TSN_SW_POLL_DELAY_US		10
 #define TSN_SW_POLL_TIMEOUT_US		5000
 
+/* Stream-destination-lookup CAM: maps a (destination MAC, VLAN ID) key
+ * to an egress port list, backing the bridge FDB. Access is indirect:
+ * load the key, value, and port-list registers, write the opcode, then
+ * set the Enable Operation bit in the control register. Poll CAM Init
+ * Done in the status register to 1 before each operation to confirm the
+ * block is ready. Enable Operation self-clears when the operation
+ * finishes.
+ */
+#define TSN_CAM_CTRL_OFFSET		0x01000
+#define TSN_CAM_STATUS_OFFSET		0x01004
+#define TSN_CAM_KEY1_OFFSET		0x01008
+#define TSN_CAM_KEY2_OFFSET		0x0100c
+#define TSN_CAM_TV1_OFFSET		0x01010
+#define TSN_CAM_TV2_OFFSET		0x01014
+#define TSN_CAM_PORT_ACT_OFFSET		0x01018
+
+/* Control register: set to start an operation, self-clears when done. */
+#define TSN_CAM_OP_ENABLE		BIT(0)
+/* Status register: reads 1 when the CAM is ready for the next operation. */
+#define TSN_CAM_STATUS_READY		BIT(0)
+#define TSN_CAM_OP_MASK			GENMASK(2, 1)
+#define TSN_CAM_OP_READ_KEY		0x0
+#define TSN_CAM_OP_ADD			0x1
+#define TSN_CAM_OP_DELETE		0x2
+#define TSN_CAM_OP_READ			0x3
+#define TSN_CAM_FOUND			BIT(7)
+#define TSN_CAM_VLAN			GENMASK(27, 16)
+#define TSN_CAM_PORT_LIST		GENMASK(10, 8)
+#define TSN_CAM_READ_KEY_ADDR		GENMASK(19, 8)
+#define TSN_CAM_MAC2_READ_KEY_BASE	0x800
+#define TSN_CAM_READ_KEY_COUNT		2048
+
+/* HW reset-default native VID; DSA's "no VLAN" (vid 0) maps onto it. */
+#define TSN_SW_DEFAULT_VID		1
+
+/* Switch Control Register: switch-wide control fields. */
+#define TSN_SW_CTRL_OFFSET		0x00004
+/* Per-port native-VLAN untag enables: strip the tag on egress when the
+ * frame VID equals the egress port's native VID. Only the wire ports are
+ * untagged; the endpoint port is left tagged so the host keeps the VID for
+ * software classification.
+ */
+#define TSN_SW_CTRL_MAC1_NATIVE_UNTAG_EN	BIT(18)
+#define TSN_SW_CTRL_MAC2_NATIVE_UNTAG_EN	BIT(19)
+/* Action for an ingress frame whose port is not in its VLAN's member
+ * list: forward to the processor or discard. Pinned to discard. This only
+ * matters once a VID's Port-List-Valid bit is set, which happens only
+ * while VLAN filtering is on.
+ */
+#define TSN_SW_CTRL_MEMBER_VIOL_MASK		GENMASK(9, 8)
+#define TSN_SW_CTRL_MEMBER_VIOL_DISCARD		0x1
+
+/* Forwarding action for a tagged unicast or multicast frame that misses
+ * both the CAM and the VLAN membership list: flood, to processor, to MAC,
+ * or discard.
+ */
+#define TSN_SW_CTRL_UCAST_MISS_MASK		GENMASK(1, 0)
+#define TSN_SW_CTRL_MCAST_MISS_MASK		GENMASK(3, 2)
+#define TSN_SW_CTRL_UCAST_MISS_FLOOD		0x1
+#define TSN_SW_CTRL_MCAST_MISS_FLOOD		0x1
+#define TSN_SW_CTRL_MISS_DISCARD		0x3
+
+/* Hardware Address Learning Control: switch-wide learning behaviour. */
+#define TSN_SW_ADDR_LEARN_OFFSET		0x00048
+/* Disable the global learning engine; set to 1 while every user port is
+ * standalone, cleared to 0 when at least one port joins a bridge. The
+ * sub-qualifier bits below are inert while this bit is set.
+ */
+#define TSN_SW_ADDR_LEARN_DISABLE		BIT(0)
+/* Learn the source MAC of untagged and priority-tagged frames against the
+ * ingress port's native VID; the reset default excludes them from learning.
+ */
+#define TSN_SW_ADDR_LEARN_UNTAGGED_EN		BIT(1)
+/* Permit learning on a VID that has no member entry programmed in the VLAN
+ * membership memory; the reset default only learns already-configured VIDs.
+ */
+#define TSN_SW_ADDR_LEARN_NO_VLAN_EN		BIT(3)
+
+/* Map a DSA port index to its bit in a switch port list. */
+#define TSN_PORT_BIT(port)		BIT(port)
+
 enum tsn_port_state {
 	TSN_PORT_STATE_DISABLED = 0,
 	TSN_PORT_STATE_BLOCKING,
 	TSN_PORT_STATE_LISTENING,
 	TSN_PORT_STATE_LEARNING,
 	TSN_PORT_STATE_FORWARDING,
+	/* Not an STP state. Writing it flushes the port's dynamic learnt
+	 * entries and leaves static FDB entries in place.
+	 */
+	TSN_PORT_STATE_FLUSH,
 };
 
 /* Per-MAC MDIO controller register window, sitting at +0x500 inside
@@ -218,6 +304,8 @@ struct xlnx_tsn_mac {
  * @nb: netdev notifier that handles NETDEV_REGISTER on each swpN
  *	to set its final MAC, and NETDEV_CHANGEADDR on the conduit
  *	to refresh the shared prefix
+ * @indirect_lock: serialises the CAM and VLAN-membership indirect
+ *		   register sequences
  * @mac: per-MAC state, indexed by user-port number (index 0 unused;
  *	 MAC1 at [1], MAC2 at [2])
  * @ptp_timer_irq: 1 PPS / RTC-overflow interrupt
@@ -239,6 +327,7 @@ struct xlnx_tsn {
 	struct net_device *conduit;
 	u8 mac_prefix[ETH_ALEN];
 	struct notifier_block nb;
+	struct mutex indirect_lock; /* serialises CAM indirect access */
 	struct xlnx_tsn_mac mac[XLNX_TSN_NUM_PORTS];
 	int ptp_timer_irq;
 	struct ptp_clock *ptp_clock;
-- 
2.34.1
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.