The PCA9545 is a 4-channel I2C mux/switch. What sets it apart from the
plain PCA9546 is its interrupt logic: one INTn input per channel and a
shared INT output, both active low, asserted whenever any channel
interrupt is active.
Model this behind a new has_irq class flag. The PCA9545 gains four named
"interrupt" GPIO inputs and an "interrupt-out" output, and its control
read-back merges the latched status nibble; the plain PCA9546/PCA9548
switches keep their previous behaviour.
Signed-off-by: Emmanuel Blot <[email protected]>
---
hw/i2c/i2c_mux_pca954x.c | 71 ++++++++++++++++++++++++++++++++++++++--
include/hw/i2c/i2c_mux_pca954x.h | 1 +
2 files changed, 70 insertions(+), 2 deletions(-)
diff --git a/hw/i2c/i2c_mux_pca954x.c b/hw/i2c/i2c_mux_pca954x.c
index c8e75da8b2..4bb078a03c 100644
--- a/hw/i2c/i2c_mux_pca954x.c
+++ b/hw/i2c/i2c_mux_pca954x.c
@@ -12,12 +12,20 @@
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
+ *
+ * Limitations:
+ * - A control write longer than one byte is rejected rather than retaining the
+ * last received byte as the datasheet specifies.
+ * - On reset the cached per-channel interrupt state is cleared; a downstream
+ * INT input that stays asserted across the reset is not re-sampled until it
+ * next toggles.
*/
#include "qemu/osdep.h"
#include "hw/i2c/i2c.h"
#include "hw/i2c/i2c_mux_pca954x.h"
#include "hw/i2c/smbus_slave.h"
+#include "hw/core/irq.h"
#include "hw/core/qdev.h"
#include "hw/core/qdev-properties.h"
#include "qemu/log.h"
@@ -26,11 +34,15 @@
#include "trace.h"
#define PCA9548_CHANNEL_COUNT 8
+#define PCA9545_CHANNEL_COUNT 4
#define PCA9546_CHANNEL_COUNT 4
/*
* struct Pca954xState - The pca954x state object.
* @control: The value written to the mux control.
+ * @int_status: Per-channel interrupt status, for parts with interrupt logic.
+ * @int_out: The mux INT output, driven low while any channel interrupt is
+ * active.
* @channel: The set of i2c channel buses that act as channels which own the
* i2c children.
*/
@@ -38,6 +50,8 @@ typedef struct Pca954xState {
SMBusDevice parent;
uint8_t control;
+ uint8_t int_status;
+ qemu_irq int_out;
bool enabled[PCA9548_CHANNEL_COUNT];
I2CBus *bus[PCA9548_CHANNEL_COUNT];
@@ -48,11 +62,13 @@ typedef struct Pca954xState {
/*
* struct Pca954xClass - The pca954x class object.
* @nchans: The number of i2c channels this device has.
+ * @has_irq: Whether the part implements interrupt logic.
*/
typedef struct Pca954xClass {
SMBusDeviceClass parent;
uint8_t nchans;
+ bool has_irq;
} Pca954xClass;
#define TYPE_PCA954X "pca954x"
@@ -116,8 +132,10 @@ static void pca954x_enable_channel(Pca954xState *s,
uint8_t enable_mask)
static void pca954x_write(Pca954xState *s, uint8_t data)
{
- s->control = data;
- pca954x_enable_channel(s, data);
+ Pca954xClass *c = PCA954X_GET_CLASS(s);
+
+ s->control = c->has_irq ? data & ((1 << c->nchans) - 1) : data;
+ pca954x_enable_channel(s, s->control);
trace_pca954x_write_bytes(data);
}
@@ -148,16 +166,45 @@ static int pca954x_write_data(SMBusDevice *d, uint8_t
*buf, uint8_t len)
static uint8_t pca954x_read_byte(SMBusDevice *d)
{
Pca954xState *s = PCA954X(d);
+ Pca954xClass *c = PCA954X_GET_CLASS(s);
uint8_t data = s->control;
+
+ /*
+ * On parts with interrupt logic the read-back byte carries the per-channel
+ * interrupt status in the upper nibble.
+ */
+ if (c->has_irq) {
+ data = (s->int_status << 4) | (s->control & 0x0f);
+ }
+
trace_pca954x_read_data(data);
return data;
}
+/*
+ * A downstream device drives one of the INTn inputs. INTn and INT are active
+ * low, and both carry the physical level: 0 is asserted, 1 is idle.
+ */
+static void pca954x_irq_handler(void *opaque, int n, int level)
+{
+ Pca954xState *s = PCA954X(opaque);
+
+ if (level) {
+ s->int_status &= ~(1 << n);
+ } else {
+ s->int_status |= (1 << n);
+ }
+
+ qemu_set_irq(s->int_out, s->int_status == 0);
+}
+
static void pca954x_enter_reset(Object *obj, ResetType type)
{
Pca954xState *s = PCA954X(obj);
/* Reset will disable all channels. */
pca954x_write(s, 0);
+ s->int_status = 0;
+ qemu_set_irq(s->int_out, 1); /* INT released */
}
I2CBus *pca954x_i2c_get_bus(I2CSlave *mux, uint8_t channel)
@@ -169,6 +216,13 @@ I2CBus *pca954x_i2c_get_bus(I2CSlave *mux, uint8_t channel)
return pca954x->bus[channel];
}
+static void pca9545_class_init(ObjectClass *klass, const void *data)
+{
+ Pca954xClass *s = PCA954X_CLASS(klass);
+ s->nchans = PCA9545_CHANNEL_COUNT;
+ s->has_irq = true;
+}
+
static void pca9546_class_init(ObjectClass *klass, const void *data)
{
Pca954xClass *s = PCA954X_CLASS(klass);
@@ -184,12 +238,20 @@ static void pca9548_class_init(ObjectClass *klass, const
void *data)
static void pca954x_realize(DeviceState *dev, Error **errp)
{
Pca954xState *s = PCA954X(dev);
+ Pca954xClass *c = PCA954X_GET_CLASS(dev);
DeviceState *d = DEVICE(s);
if (s->name) {
d->id = g_strdup(s->name);
} else {
d->id = g_strdup_printf("pca954x[%x]", s->parent.i2c.address);
}
+
+ if (c->has_irq) {
+ /* One INTn input per channel, plus the shared INT output. */
+ qdev_init_gpio_in_named(dev, pca954x_irq_handler, "interrupt",
+ c->nchans);
+ qdev_init_gpio_out_named(dev, &s->int_out, "interrupt-out", 1);
+ }
}
static void pca954x_init(Object *obj)
@@ -242,6 +304,11 @@ static const TypeInfo pca954x_info[] = {
.class_init = pca954x_class_init,
.abstract = true,
},
+ {
+ .name = TYPE_PCA9545,
+ .parent = TYPE_PCA954X,
+ .class_init = pca9545_class_init,
+ },
{
.name = TYPE_PCA9546,
.parent = TYPE_PCA954X,
diff --git a/include/hw/i2c/i2c_mux_pca954x.h b/include/hw/i2c/i2c_mux_pca954x.h
index 3dd25ec983..19a1050505 100644
--- a/include/hw/i2c/i2c_mux_pca954x.h
+++ b/include/hw/i2c/i2c_mux_pca954x.h
@@ -3,6 +3,7 @@
#include "hw/i2c/i2c.h"
+#define TYPE_PCA9545 "pca9545"
#define TYPE_PCA9546 "pca9546"
#define TYPE_PCA9548 "pca9548"