aboutsummaryrefslogtreecommitdiff
path: root/hw/gpio/pcf8574.c
blob: 208efe69ea5ca59d44ffb446f056a8563547e0db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/* 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;
    device_class_set_legacy_reset(dc, 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);