Exercise the PCA9545 4-channel I2C mux: channel selection through the
control byte, upper-nibble read-back, and interrupt logic.
Signed-off-by: Emmanuel Blot <[email protected]>
---
tests/qtest/meson.build | 1 +
tests/qtest/pca9545-test.c | 116 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 117 insertions(+)
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 200abf6f36..e05d8a07b9 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -336,6 +336,7 @@ qos_test_ss.add(
'ne2000-test.c',
'tulip-test.c',
'nvme-test.c',
+ 'pca9545-test.c',
'pca9552-test.c',
'pca9554-test.c',
'pca9555-test.c',
diff --git a/tests/qtest/pca9545-test.c b/tests/qtest/pca9545-test.c
new file mode 100644
index 0000000000..91f56db92f
--- /dev/null
+++ b/tests/qtest/pca9545-test.c
@@ -0,0 +1,116 @@
+/*
+ * QTest testcase for the PCA9545 4-channel I2C switch with interrupt logic
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest-single.h"
+#include "libqos/qgraph.h"
+#include "libqos/i2c.h"
+
+#define PCA9545_TEST_ID "pca9545-test"
+#define PCA9545_TEST_ADDR 0x70
+#define PCA9545_TEST_PATH "/machine/peripheral/" PCA9545_TEST_ID
+
+/* INTn and INT are active low: level 0 is asserted. */
+#define INT_ASSERTED 0
+#define INT_RELEASED 1
+
+/* The mux has no register pointer: a read/write moves a single control byte.
*/
+static uint8_t pca9545_get(QI2CDevice *dev)
+{
+ uint8_t val;
+
+ qi2c_recv(dev, &val, 1);
+ return val;
+}
+
+static void pca9545_set(QI2CDevice *dev, uint8_t val)
+{
+ qi2c_send(dev, &val, 1);
+}
+
+static void pca9545_set_int(unsigned chan, int level)
+{
+ qtest_set_irq_in(global_qtest, PCA9545_TEST_PATH, "interrupt", chan,
level);
+}
+
+/* Channel selection is the lower nibble; the upper nibble reads back 0. */
+static void test_channel_select(void *obj, void *data, QGuestAllocator *alloc)
+{
+ QI2CDevice *dev = (QI2CDevice *)obj;
+
+ g_assert_cmphex(pca9545_get(dev), ==, 0x00);
+
+ pca9545_set(dev, 0x05); /* channels 0 and 2 */
+ g_assert_cmphex(pca9545_get(dev), ==, 0x05);
+
+ pca9545_set(dev, 0x0f); /* all channels */
+ g_assert_cmphex(pca9545_get(dev), ==, 0x0f);
+
+ pca9545_set(dev, 0x00);
+ g_assert_cmphex(pca9545_get(dev), ==, 0x00);
+}
+
+/*
+ * The interrupt status is reported in the upper nibble (INT3..INT0) and is
+ * latched regardless of which channels are selected (PCA9545A Table 5).
+ */
+static void test_interrupt_status(void *obj, void *data, QGuestAllocator
*alloc)
+{
+ QI2CDevice *dev = (QI2CDevice *)obj;
+
+ pca9545_set_int(1, INT_ASSERTED);
+ g_assert_cmphex(pca9545_get(dev), ==, 0x20);
+
+ pca9545_set(dev, 0x01);
+ pca9545_set_int(3, INT_ASSERTED);
+ g_assert_cmphex(pca9545_get(dev), ==, 0xa1); /* INT3 | INT1 | channel 0 */
+
+ pca9545_set_int(1, INT_RELEASED);
+ g_assert_cmphex(pca9545_get(dev), ==, 0x81); /* INT3 | channel 0 */
+}
+
+/* The INT output is pulled low while any channel interrupt is active. */
+static void test_interrupt_output(void *obj, void *data, QGuestAllocator
*alloc)
+{
+ qtest_irq_intercept_out_named(global_qtest, PCA9545_TEST_PATH,
+ "interrupt-out");
+
+ /*
+ * Interception starts with no recorded level, so drive an idle input
+ * first to make the mux publish the released state of its output.
+ */
+ pca9545_set_int(0, INT_RELEASED);
+ g_assert_true(get_irq(0));
+
+ pca9545_set_int(0, INT_ASSERTED);
+ g_assert_false(get_irq(0));
+
+ pca9545_set_int(2, INT_ASSERTED);
+ g_assert_false(get_irq(0));
+
+ pca9545_set_int(0, INT_RELEASED);
+ g_assert_false(get_irq(0));
+ pca9545_set_int(2, INT_RELEASED);
+ g_assert_true(get_irq(0));
+}
+
+static void pca9545_register_nodes(void)
+{
+ QOSGraphEdgeOptions opts = {
+ .extra_device_opts = "id=" PCA9545_TEST_ID ",address=0x70"
+ };
+ add_qi2c_address(&opts, &(QI2CAddress) { PCA9545_TEST_ADDR });
+
+ qos_node_create_driver("pca9545", i2c_device_create);
+ qos_node_consumes("pca9545", "i2c-bus", &opts);
+
+ qos_add_test("channel-select", "pca9545", test_channel_select, NULL);
+ qos_add_test("interrupt-status", "pca9545", test_interrupt_status, NULL);
+ qos_add_test("interrupt-output", "pca9545", test_interrupt_output, NULL);
+}
+libqos_init(pca9545_register_nodes);