Hi Corey,
thank you for your response. Could you give me some hints how to make IO operations non-blocking in QEMU? Is there a code reference in the source code of QEMU I could use? Karol ________________________________ From: Corey Minyard <tcminy...@gmail.com> on behalf of Corey Minyard <miny...@acm.org> Sent: Thursday, March 23, 2023 5:03 PM To: Karol Nowak <k...@spyro-soft.com> Cc: qemu-devel@nongnu.org <qemu-devel@nongnu.org>; phi...@linaro.org <phi...@linaro.org>; c...@kaod.org <c...@kaod.org> Subject: Re: [RFC PATCH v1] hw/misc: add i2c slave device that passes i2c ops outside [You don't often get email from miny...@acm.org. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ] ⚠ This email originates from outside the organization or the sender could not be verified. On Thu, Mar 23, 2023 at 10:09:02AM +0000, Karol Nowak wrote: > Hi, > > There is a feature I prepared which may be practical for some QEMU users. > > The feature provides a new I2C slave device > that prepares a message depending what i2c-slave callback was called > and sends it outside of QEMU through the character device to a client > that receives that message, processes it and send back a response. > Thanks to that feature, > a user can emulate a logic of I2C device outside of QEMU. > The message contains 3 bytes ended with CRLF: BBB\r\l > Basically, the I2C slave does 4 steps in each i2c-slave callback: > * encode > * send > * receive > * decode > > I put more details in esp32_i2c_tcp_slave.c > and also provided a demo client in python that uses TCP. > > The feature still needs some improvements, but the question is: > * Do you find the feature useful? Someone else has proposed this before with a patch, and it was actually pretty complete and mostly ok, but I pointed out an issue and never heard back from them. This feature is something that might be nice. As you say, this needs some improvements. Some I would point out: Obviously this can't be named esp32, it needs to be general. All the I/O (reading and writing) has to be non-blocking. I/O handling in qemu is single-threaded, if you block anywhere you basically stop qemu. You need to implement something where whatever you do (like handling a NAK, for instance) it doesn't block qemu. The protocol you have implemented is basically an extension of the QEMU protocol. That's probably not ideal, it would be best to think about a general protocol for extending I2C over a TCP connection. A lot of the details of the QEMU implementation is probably not necessary over a TCP connection. -corey > > > NOTE: > The feature originally was prepared for espressif/qemu > that's why there are references to esp32 > > > Signed-off-by: Karol Nowak <k...@spyro-soft.com> > --- > hw/misc/esp32_i2c_tcp_slave.c | 288 ++++++++++++++++++++++++++ > include/hw/misc/esp32_i2c_tcp_slave.h | 19 ++ > tests/i2c-tcp-demo/i2c-tcp-demo.py | 133 ++++++++++++ > 3 files changed, 440 insertions(+) > create mode 100644 hw/misc/esp32_i2c_tcp_slave.c > create mode 100644 include/hw/misc/esp32_i2c_tcp_slave.h > create mode 100644 tests/i2c-tcp-demo/i2c-tcp-demo.py > > diff --git a/hw/misc/esp32_i2c_tcp_slave.c b/hw/misc/esp32_i2c_tcp_slave.c > new file mode 100644 > index 0000000000..db3b6d366a > --- /dev/null > +++ b/hw/misc/esp32_i2c_tcp_slave.c > @@ -0,0 +1,288 @@ > +#include "qemu/osdep.h" > +#include "qemu/error-report.h" > +#include "qemu/log.h" > +#include "hw/i2c/i2c.h" > +#include "hw/irq.h" > +#include "hw/misc/esp32_i2c_tcp_slave.h" > +#include "qemu/module.h" > + > +#include "qapi/qmp/json-writer.h" > +#include "chardev/char-fe.h" > +#include "io/channel-socket.h" > +#include "chardev/char-io.h" > +#include "chardev/char-socket.h" > +#include "qapi/error.h" > + > +/* > + * Description: > + * To allow to emulate a I2C slave device which is not supported by QEMU, > + * a new I2C slave device was created that encapsulates I2C operations > + * and passes them through a selected chardev to the host > + * where a client resides that implements a logic of emulated device. > + * > + * > + * Architecture: > + * --------------------------- > + * | QEMU | > + * | | ----------------------- > + * | ESP32 Firmware writes | | | > + * | to I2C Slave | | I2C Slave Emulation | > + * | | | | > + * | -----------------------&---------&---- | > + * | | I2C Slave at 0x7F & tcp & recv msg | > + * | -----------------------&---------&---- process msg | > + * | | | send respone | > + * | | | | > + * | | | | > + * --------------------------- |---------------------- > + * > + * > + * Syntax & protocol: > + * QEMU I2C Slave sends a msg in following format: BBB\r\n > + * where each 'B' represents a single byte 0-255 > + * QEMU I2C Slave expects a respone message in the same format as fast > as possible > + * Example: > + * req: 0x45 0x01 0x00 \r\n > + * resp: 0x45 0x01 0x00 \r\n > + * > + * The format BBB\r\n > + * first 'B' is a message type > + * second 'B' is a data value > + * third 'B' is an error value (not used at the moment) > + * > + * There are three types of message > + * 'E' or 0x45 - Event: > + * 'S' or 0x53 - Send: byte sent to emulated I2C Slave > + * 'R' or 0x52 - Recv: byte to be received by I2C Master > + * > + * > + * 'E' message > + * second byte is an event type: > + * 0x0: I2C_START_RECV > + * 0x1: I2C_START_SEND > + * 0x2: I2C_START_SEND_ASYNC > + * 0x3: I2C_FINISH > + * 0x4: I2C_NACK > + * > + * Example: > + * 0x45 0x01 0x00 - start send > + * 0x45 0x03 0x00 - finish > + * > + * In case of 'E' message, a response is the same as a request message > + * > + * 'S' message > + * second byte is a byte transmitted from I2C Master to I2C slave > device > + * the byte to by processed by I2C Slave Device > + * > + * Example: > + * 0x53 0x20 0x00 > + * > + * In case of 'S' message, a response is the same as a request message > + * > + * 'R' message > + * the I2C Master expect a byte from the emulated i2c slave device > + * A client has to modify the second byte of the request message > + * and send it back as a response. > + * > + * Example: > + * req: 0x52 0x00 0x00 > + * resp: 0x52 0x11 0x00 > + * > + * > + * Examples of Transmission: > + * 1) i2cset -c 0x7F -r 0x20 0x11 0x22 0x33 0x44 0x55 > + * req: 45 01 00 > + * resp: 45 01 00 > + * > + * req: 53 20 00 > + * resp: 53 20 00 > + * > + * req: 53 11 00 > + * resp: 53 11 00 > + * > + * req: 53 22 00 > + * resp: 53 22 00 > + * > + * req: 53 33 00 > + * resp: 53 33 00 > + * > + * req: 53 44 00 > + * resp: 53 44 00 > + * > + * req: 53 55 00 > + * resp: 53 55 00 > + * > + * req: 45 03 00 > + * resp: 45 03 00 > + * > + * 2) i2cget -c 0x7F -r 0x20 -l 0x03 > + * req: 45 01 00 > + * resp: 45 01 00 > + * > + * req: 53 20 00 > + * resp: 53 20 00 > + * > + * req: 45 03 00 > + * resp: 45 03 00 > + * > + * req: 45 00 00 > + * resp: 45 00 00 > + * > + * req: 52 00 00 > + * resp: 52 11 00 > + * > + * req: 52 00 00 > + * resp: 52 22 00 > + * > + * req: 52 00 00 > + * resp: 52 33 00 > + * > + * req: 45 03 00 > + * resp: 45 03 00 > + * > + * > + * To start i2c.socket server, set QEMU param: > + * -chardev > socket,port=16001,wait=no,host=localhost,server=on,ipv4=on,id=i2c.socket > + * > + * Simple demo I2C Slave Emulation in Python: > + * tests/i2c-tcp-demo/i2c-tcp-demo.py > + * > + * Limitations: > + * - there is no recv timeout which may lead to qemu hang > + * > + */ > + > +#define CHARDEV_NAME "i2c.socket" > + > +static Chardev *chardev; > +static CharBackend char_backend; > +static bool chardev_open; > + > +typedef struct { > + uint8_t id; > + uint8_t byte; > + uint8_t err; > +} packet; > + > +static int chr_can_receive(void *opaque) > +{ > + return CHR_READ_BUF_LEN; > +} > + > +static void chr_event(void *opaque, QEMUChrEvent event) > +{ > + switch (event) { > + case CHR_EVENT_OPENED: > + qemu_log("connected\n"); > + chardev_open = true; > + break; > + case CHR_EVENT_CLOSED: > + qemu_log("disconnected\n"); > + chardev_open = false; > + break; > + case CHR_EVENT_BREAK: > + case CHR_EVENT_MUX_IN: > + case CHR_EVENT_MUX_OUT: > + /* Ignore */ > + break; > + } > +} > + > +static void send_packet(packet *p) > +{ > + static const char *PACKET_FMT = "%c%c%c\r\n"; > + static char buff[32]; > + > + /* encode */ > + int len = snprintf(buff, sizeof(buff), PACKET_FMT, p->id, p->byte, > p->err); > + > + /* send */ > + qemu_chr_fe_write_all(&char_backend, (uint8_t *)buff, len); > + > + /* receive */ > + qemu_chr_fe_read_all(&char_backend, (uint8_t *)buff, len); > + > + /* decode */ > + sscanf(buff, PACKET_FMT, &p->id, &p->byte, &p->err); > +} > + > +static uint8_t slave_rx(I2CSlave *i2c) > +{ > + packet p = {.id = 'R', > + .byte = 0, > + .err = 0}; > + > + send_packet(&p); > + > + return p.byte; > +} > + > +static int slave_tx(I2CSlave *i2c, uint8_t data) > +{ > + packet p = {.id = 'S', > + .byte = data, > + .err = 0}; > + > + send_packet(&p); > + > + return 0; > +} > + > +static int slave_event(I2CSlave *i2c, enum i2c_event event) > +{ > + packet p = {.id = 'E', > + .byte = event, > + .err = 0}; > + > + send_packet(&p); > + > + return 0; > +} > + > +static void slave_realize(DeviceState *dev, Error **errp) > +{ > +} > + > +static void slave_init(Object *obj) > +{ > + Error *err = NULL; > + chardev = qemu_chr_find(CHARDEV_NAME); > + if (!chardev) { > + error_report("chardev '%s' not found", CHARDEV_NAME); > + return; > + } > + > + if (!qemu_chr_fe_init(&char_backend, chardev, &err)) { > + error_report_err(err); > + return; > + } > + > + qemu_chr_fe_set_handlers(&char_backend, chr_can_receive, NULL, chr_event, > + NULL, NULL, NULL, true); > +} > + > +static void slave_class_init(ObjectClass *klass, void *data) > +{ > + DeviceClass *dc = DEVICE_CLASS(klass); > + I2CSlaveClass *k = I2C_SLAVE_CLASS(klass); > + > + dc->realize = slave_realize; > + k->event = slave_event; > + k->recv = slave_rx; > + k->send = slave_tx; > +} > + > +static const TypeInfo esp32_i2c_tcp_info = { > + .name = TYPE_ESP32_I2C_TCP, > + .parent = TYPE_I2C_SLAVE, > + .instance_size = sizeof(ESP32_I2C_TCP_State), > + .instance_init = slave_init, > + .class_init = slave_class_init, > +}; > + > +static void esp32_i2c_tcp_type_init(void) > +{ > + type_register_static(&esp32_i2c_tcp_info); > +} > + > +type_init(esp32_i2c_tcp_type_init); > diff --git a/include/hw/misc/esp32_i2c_tcp_slave.h > b/include/hw/misc/esp32_i2c_tcp_slave.h > new file mode 100644 > index 0000000000..e36bac7ffe > --- /dev/null > +++ b/include/hw/misc/esp32_i2c_tcp_slave.h > @@ -0,0 +1,19 @@ > +/* > + */ > +#ifndef QEMU_ESP32_I2C_TCP_SLAVE_H > +#define QEMU_ESP32_I2C_TCP_SLAVE_H > + > +#include "hw/i2c/i2c.h" > +#include "qom/object.h" > + > +#define TYPE_ESP32_I2C_TCP "esp32_i2c_tcp" > +OBJECT_DECLARE_SIMPLE_TYPE(ESP32_I2C_TCP_State, ESP32_I2C_TCP) > + > +/** > + */ > +struct ESP32_I2C_TCP_State { > + /*< private >*/ > + I2CSlave i2c; > +}; > + > +#endif > diff --git a/tests/i2c-tcp-demo/i2c-tcp-demo.py > b/tests/i2c-tcp-demo/i2c-tcp-demo.py > new file mode 100644 > index 0000000000..d4bec457f3 > --- /dev/null > +++ b/tests/i2c-tcp-demo/i2c-tcp-demo.py > @@ -0,0 +1,133 @@ > +import json > +from twisted.internet import task > +from twisted.internet.defer import Deferred > +from twisted.internet.protocol import ClientFactory > +from twisted.protocols.basic import LineReceiver > +from dataclasses import dataclass > +from enum import Enum > + > +# i2cset -c 0x7F -r 0x20 0x11 0x22 0x33 0x44 0x55 > +# i2cget -c 0x7F -r 0x20 -l 0x0A > + > +HOST = "localhost" > +PORT = 16001 > + > + > +class EVENT(Enum): > + I2C_START_RECV = 0 > + I2C_START_SEND = 1 > + I2C_START_SEND_ASYNC = 2 > + I2C_FINISH = 3 > + I2C_NACK = 4 > + > + > +@dataclass > +class I2CSlave: > + mem: bytearray = bytearray(256) > + mem_addr: int = 0 > + curr_addr: int = 0 > + first_send: bool = True > + recv_conuter: int = 0 > + > + > +i2cslave = I2CSlave() > + > + > +def dump_mem(): > + print("Mem:") > + bytes_per_row = 32 > + rows = int(256 / bytes_per_row) > + for i in range(0, rows): > + begin = i*bytes_per_row > + end = begin+bytes_per_row > + prefix = hex(begin) > + if i == 0: > + prefix = "0x00" > + print(prefix + ": " + i2cslave.mem[begin:end].hex(" ")) > + > + print("\n") > + > + > +def event_handler(packet): > + evt = EVENT(packet[1]) > + print("Event handler: " + evt.name) > + > + if evt is EVENT.I2C_FINISH: > + i2cslave.recv_conuter = 0 > + i2cslave.first_send = True > + dump_mem() > + > + return packet > + > + > +def recv_handler(packet): > + print("Recv handler: byte number " + str(i2cslave.recv_conuter) + > + " from addr=" + hex(i2cslave.mem_addr) + > + ", val=" + hex(i2cslave.mem[i2cslave.mem_addr])) > + i2cslave.recv_conuter += 1 > + resp = bytearray(packet) > + resp[1] = i2cslave.mem[i2cslave.mem_addr] > + i2cslave.mem_addr += 1 > + if i2cslave.mem_addr == 256: > + i2cslave.mem_addr = 0 > + return bytes(resp) > + > + > +def send_handler(packet): > + print("Send handler: ", end='') > + if i2cslave.first_send == True: > + print("address byte: ", hex(packet[1])) > + i2cslave.mem_addr = packet[1] > + i2cslave.first_send = False > + else: > + print("data byte: ", hex(packet[1])) > + i2cslave.mem[i2cslave.mem_addr] = packet[1] > + i2cslave.mem_addr += 1 > + if i2cslave.mem_addr == 256: > + i2cslave.mem_addr = 0 > + return packet > + > + > +handlers = {'E': event_handler, 'R': recv_handler, 'S': send_handler} > + > + > +class PacketReceiver(LineReceiver): > + def __init__(self) -> None: > + super().__init__() > + > + def connectionMade(self): > + print("connected") > + > + def lineReceived(self, line): > + # print(line.hex(" ")) > + resp = line > + if len(line) == 3: > + resp = handlers[chr(line[0])](line) > + > + self.sendLine(resp) > + > + > +class PacketReceiverFactory(ClientFactory): > + protocol = PacketReceiver > + > + def __init__(self): > + self.done = Deferred() > + > + def clientConnectionFailed(self, connector, reason): > + print("connection failed:", reason.getErrorMessage()) > + self.done.errback(reason) > + > + def clientConnectionLost(self, connector, reason): > + print("connection lost:", reason.getErrorMessage()) > + self.done.callback(None) > + > + > +def main(reactor): > + dump_mem() > + factory = PacketReceiverFactory() > + reactor.connectTCP(HOST, PORT, factory) > + return factory.done > + > + > +if __name__ == "__main__": > + task.react(main) > -- > 2.34.1 > > > Please consider the environment before printing this e-mail. > ________________________________ > This e-mail (including any attachments) is intended solely for the use of the > individual or entity to which it is addressed and may contain confidential > information. This message is not a binding agreement and does not conclude an > agreement without the express confirmation of the sender's superior or a > director of the company. > If you are not the intended recipient, you should immediately notify the > sender and delete the message along all the attachments. Any disclosure, > copying, distribution or any other action is prohibited and may be illegal. > No e-mail transmission can be guaranteed to be 100% secure or error-free, as > information could be intercepted, corrupted, lost, destroyed, arrive late or > incomplete, or contain viruses. Although Spyrosoft has taken precautions to > ensure that this e-mail is free from viruses, the company does not accept > liability for any errors or omissions in the content of this message, which > arise as a result of the e-mail transmission. This e-mail is deemed to be > professional in nature. Spyrosoft does not permit the employees to send > emails which contravene provisions of the law.