|  | /* SPDX-License-Identifier: GPL-2.0-only */ | 
|  |  | 
|  | /* | 
|  | * NXP PCF8574 8-port I2C GPIO expansion chip. | 
|  | * Copyright (c) 2024 KNS Group (YADRO). | 
|  | * Written by Dmitrii Sharikhin <d.sharikhin@yadro.com> | 
|  | */ | 
|  |  | 
|  | #include "qemu/osdep.h" | 
|  | #include "hw/i2c/i2c.h" | 
|  | #include "hw/gpio/pcf8574.h" | 
|  | #include "hw/irq.h" | 
|  | #include "migration/vmstate.h" | 
|  | #include "qemu/log.h" | 
|  | #include "qemu/module.h" | 
|  | #include "qom/object.h" | 
|  |  | 
|  | /* | 
|  | * PCF8574 and compatible chips incorporate quasi-bidirectional | 
|  | * IO. Electrically it means that device sustain pull-up to line | 
|  | * unless IO port is configured as output _and_ driven low. | 
|  | * | 
|  | * IO access is implemented as simple I2C single-byte read | 
|  | * or write operation. So, to configure line to input user write 1 | 
|  | * to corresponding bit. To configure line to output and drive it low | 
|  | * user write 0 to corresponding bit. | 
|  | * | 
|  | * In essence, user can think of quasi-bidirectional IO as | 
|  | * open-drain line, except presence of builtin rising edge acceleration | 
|  | * embedded in PCF8574 IC | 
|  | * | 
|  | * PCF8574 has interrupt request line, which is being pulled down when | 
|  | * port line state differs from last read. Port read operation clears | 
|  | * state and INT line returns to high state via pullup. | 
|  | */ | 
|  |  | 
|  | OBJECT_DECLARE_SIMPLE_TYPE(PCF8574State, PCF8574) | 
|  |  | 
|  | #define PORTS_COUNT (8) | 
|  |  | 
|  | struct PCF8574State { | 
|  | I2CSlave parent_obj; | 
|  | uint8_t  lastrq;     /* Last requested state. If changed - assert irq */ | 
|  | uint8_t  input;      /* external electrical line state */ | 
|  | uint8_t  output;     /* Pull-up (1) or drive low (0) on bit */ | 
|  | qemu_irq handler[PORTS_COUNT]; | 
|  | qemu_irq intrq;      /* External irq request */ | 
|  | }; | 
|  |  | 
|  | static void pcf8574_reset(DeviceState *dev) | 
|  | { | 
|  | PCF8574State *s = PCF8574(dev); | 
|  | s->lastrq = MAKE_64BIT_MASK(0, PORTS_COUNT); | 
|  | s->input  = MAKE_64BIT_MASK(0, PORTS_COUNT); | 
|  | s->output = MAKE_64BIT_MASK(0, PORTS_COUNT); | 
|  | } | 
|  |  | 
|  | static inline uint8_t pcf8574_line_state(PCF8574State *s) | 
|  | { | 
|  | /* we driving line low or external circuit does that */ | 
|  | return s->input & s->output; | 
|  | } | 
|  |  | 
|  | static uint8_t pcf8574_rx(I2CSlave *i2c) | 
|  | { | 
|  | PCF8574State *s = PCF8574(i2c); | 
|  | uint8_t linestate = pcf8574_line_state(s); | 
|  | if (s->lastrq != linestate) { | 
|  | s->lastrq = linestate; | 
|  | if (s->intrq) { | 
|  | qemu_set_irq(s->intrq, 1); | 
|  | } | 
|  | } | 
|  | return linestate; | 
|  | } | 
|  |  | 
|  | static int pcf8574_tx(I2CSlave *i2c, uint8_t data) | 
|  | { | 
|  | PCF8574State *s = PCF8574(i2c); | 
|  | uint8_t prev; | 
|  | uint8_t diff; | 
|  | uint8_t actual; | 
|  | int line = 0; | 
|  |  | 
|  | prev = pcf8574_line_state(s); | 
|  | s->output = data; | 
|  | actual = pcf8574_line_state(s); | 
|  |  | 
|  | for (diff = (actual ^ prev); diff; diff &= ~(1 << line)) { | 
|  | line = ctz32(diff); | 
|  | if (s->handler[line]) { | 
|  | qemu_set_irq(s->handler[line], (actual >> line) & 1); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (s->intrq) { | 
|  | qemu_set_irq(s->intrq, actual == s->lastrq); | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const VMStateDescription vmstate_pcf8574 = { | 
|  | .name               = "pcf8574", | 
|  | .version_id         = 0, | 
|  | .minimum_version_id = 0, | 
|  | .fields = (VMStateField[]) { | 
|  | VMSTATE_I2C_SLAVE(parent_obj, PCF8574State), | 
|  | VMSTATE_UINT8(lastrq, PCF8574State), | 
|  | VMSTATE_UINT8(input,  PCF8574State), | 
|  | VMSTATE_UINT8(output, PCF8574State), | 
|  | VMSTATE_END_OF_LIST() | 
|  | } | 
|  | }; | 
|  |  | 
|  | static void pcf8574_gpio_set(void *opaque, int line, int level) | 
|  | { | 
|  | PCF8574State *s = (PCF8574State *) opaque; | 
|  | assert(line >= 0 && line < ARRAY_SIZE(s->handler)); | 
|  |  | 
|  | if (level) { | 
|  | s->input |=  (1 << line); | 
|  | } else { | 
|  | s->input &= ~(1 << line); | 
|  | } | 
|  |  | 
|  | if (pcf8574_line_state(s) != s->lastrq && s->intrq) { | 
|  | qemu_set_irq(s->intrq, 0); | 
|  | } | 
|  | } | 
|  |  | 
|  | static void pcf8574_realize(DeviceState *dev, Error **errp) | 
|  | { | 
|  | PCF8574State *s = PCF8574(dev); | 
|  |  | 
|  | qdev_init_gpio_in(dev, pcf8574_gpio_set, ARRAY_SIZE(s->handler)); | 
|  | qdev_init_gpio_out(dev, s->handler, ARRAY_SIZE(s->handler)); | 
|  | qdev_init_gpio_out_named(dev, &s->intrq, "nINT", 1); | 
|  | } | 
|  |  | 
|  | static void pcf8574_class_init(ObjectClass *klass, void *data) | 
|  | { | 
|  | DeviceClass   *dc = DEVICE_CLASS(klass); | 
|  | I2CSlaveClass *k  = I2C_SLAVE_CLASS(klass); | 
|  |  | 
|  | k->recv     = pcf8574_rx; | 
|  | k->send     = pcf8574_tx; | 
|  | dc->realize = pcf8574_realize; | 
|  | dc->reset   = pcf8574_reset; | 
|  | dc->vmsd    = &vmstate_pcf8574; | 
|  | } | 
|  |  | 
|  | static const TypeInfo pcf8574_infos[] = { | 
|  | { | 
|  | .name          = TYPE_PCF8574, | 
|  | .parent        = TYPE_I2C_SLAVE, | 
|  | .instance_size = sizeof(PCF8574State), | 
|  | .class_init    = pcf8574_class_init, | 
|  | } | 
|  | }; | 
|  |  | 
|  | DEFINE_TYPES(pcf8574_infos); |