/* * Broadcom Serial Controller (BSC) * * Copyright (c) 2024 Rayhan Faizel * * SPDX-License-Identifier: MIT * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "qemu/osdep.h" #include "qemu/log.h" #include "hw/i2c/bcm2835_i2c.h" #include "hw/irq.h" #include "migration/vmstate.h" static void bcm2835_i2c_update_interrupt(BCM2835I2CState *s) { int do_interrupt = 0; /* Interrupt on RXR (Needs reading) */ if (s->c & BCM2835_I2C_C_INTR && s->s & BCM2835_I2C_S_RXR) { do_interrupt = 1; } /* Interrupt on TXW (Needs writing) */ if (s->c & BCM2835_I2C_C_INTT && s->s & BCM2835_I2C_S_TXW) { do_interrupt = 1; } /* Interrupt on DONE (Transfer complete) */ if (s->c & BCM2835_I2C_C_INTD && s->s & BCM2835_I2C_S_DONE) { do_interrupt = 1; } qemu_set_irq(s->irq, do_interrupt); } static void bcm2835_i2c_begin_transfer(BCM2835I2CState *s) { int direction = s->c & BCM2835_I2C_C_READ; if (i2c_start_transfer(s->bus, s->a, direction)) { s->s |= BCM2835_I2C_S_ERR; } s->s |= BCM2835_I2C_S_TA; if (direction) { s->s |= BCM2835_I2C_S_RXR | BCM2835_I2C_S_RXD; } else { s->s |= BCM2835_I2C_S_TXW; } } static void bcm2835_i2c_finish_transfer(BCM2835I2CState *s) { /* * STOP is sent when DLEN counts down to zero. * * https://github.com/torvalds/linux/blob/v6.7/drivers/i2c/busses/i2c-bcm2835.c#L223-L261 * It is possible to initiate repeated starts on real hardware. * However, this requires sending another ST request before the bytes in * TX FIFO are shifted out. * * This is not emulated currently. */ i2c_end_transfer(s->bus); s->s |= BCM2835_I2C_S_DONE; /* Ensure RXD is cleared, otherwise the driver registers an error */ s->s &= ~(BCM2835_I2C_S_TA | BCM2835_I2C_S_RXR | BCM2835_I2C_S_TXW | BCM2835_I2C_S_RXD); } static uint64_t bcm2835_i2c_read(void *opaque, hwaddr addr, unsigned size) { BCM2835I2CState *s = opaque; uint32_t readval = 0; switch (addr) { case BCM2835_I2C_C: readval = s->c; break; case BCM2835_I2C_S: readval = s->s; break; case BCM2835_I2C_DLEN: readval = s->dlen; break; case BCM2835_I2C_A: readval = s->a; break; case BCM2835_I2C_FIFO: /* We receive I2C messages directly instead of using FIFOs */ if (s->s & BCM2835_I2C_S_TA) { readval = i2c_recv(s->bus); s->dlen -= 1; if (s->dlen == 0) { bcm2835_i2c_finish_transfer(s); } } bcm2835_i2c_update_interrupt(s); break; case BCM2835_I2C_DIV: readval = s->div; break; case BCM2835_I2C_DEL: readval = s->del; break; case BCM2835_I2C_CLKT: readval = s->clkt; break; default: qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset 0x%" HWADDR_PRIx "\n", __func__, addr); } return readval; } static void bcm2835_i2c_write(void *opaque, hwaddr addr, uint64_t value, unsigned int size) { BCM2835I2CState *s = opaque; uint32_t writeval = value; switch (addr) { case BCM2835_I2C_C: /* ST is a one-shot operation; it must read back as 0 */ s->c = writeval & ~BCM2835_I2C_C_ST; /* Start transfer */ if (writeval & (BCM2835_I2C_C_ST | BCM2835_I2C_C_I2CEN)) { bcm2835_i2c_begin_transfer(s); /* * Handle special case where transfer starts with zero data length. * Required for zero length i2c quick messages to work. */ if (s->dlen == 0) { bcm2835_i2c_finish_transfer(s); } } bcm2835_i2c_update_interrupt(s); break; case BCM2835_I2C_S: if (writeval & BCM2835_I2C_S_DONE && s->s & BCM2835_I2C_S_DONE) { /* When DONE is cleared, DLEN should read last written value. */ s->dlen = s->last_dlen; } /* Clear DONE, CLKT and ERR by writing 1 */ s->s &= ~(writeval & (BCM2835_I2C_S_DONE | BCM2835_I2C_S_ERR | BCM2835_I2C_S_CLKT)); break; case BCM2835_I2C_DLEN: s->dlen = writeval; s->last_dlen = writeval; break; case BCM2835_I2C_A: s->a = writeval; break; case BCM2835_I2C_FIFO: /* We send I2C messages directly instead of using FIFOs */ if (s->s & BCM2835_I2C_S_TA) { if (s->s & BCM2835_I2C_S_TXD) { if (!i2c_send(s->bus, writeval & 0xff)) { s->dlen -= 1; } else { s->s |= BCM2835_I2C_S_ERR; } } if (s->dlen == 0) { bcm2835_i2c_finish_transfer(s); } } bcm2835_i2c_update_interrupt(s); break; case BCM2835_I2C_DIV: s->div = writeval; break; case BCM2835_I2C_DEL: s->del = writeval; break; case BCM2835_I2C_CLKT: s->clkt = writeval; break; default: qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset 0x%" HWADDR_PRIx "\n", __func__, addr); } } static const MemoryRegionOps bcm2835_i2c_ops = { .read = bcm2835_i2c_read, .write = bcm2835_i2c_write, .endianness = DEVICE_NATIVE_ENDIAN, .valid = { .min_access_size = 4, .max_access_size = 4, }, }; static void bcm2835_i2c_realize(DeviceState *dev, Error **errp) { BCM2835I2CState *s = BCM2835_I2C(dev); s->bus = i2c_init_bus(dev, NULL); memory_region_init_io(&s->iomem, OBJECT(dev), &bcm2835_i2c_ops, s, TYPE_BCM2835_I2C, 0x24); sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem); sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq); } static void bcm2835_i2c_reset(DeviceState *dev) { BCM2835I2CState *s = BCM2835_I2C(dev); /* Reset values according to BCM2835 Peripheral Documentation */ s->c = 0x0; s->s = BCM2835_I2C_S_TXD | BCM2835_I2C_S_TXE; s->dlen = 0x0; s->a = 0x0; s->div = 0x5dc; s->del = 0x00300030; s->clkt = 0x40; } static const VMStateDescription vmstate_bcm2835_i2c = { .name = TYPE_BCM2835_I2C, .version_id = 1, .minimum_version_id = 1, .fields = (const VMStateField[]) { VMSTATE_UINT32(c, BCM2835I2CState), VMSTATE_UINT32(s, BCM2835I2CState), VMSTATE_UINT32(dlen, BCM2835I2CState), VMSTATE_UINT32(a, BCM2835I2CState), VMSTATE_UINT32(div, BCM2835I2CState), VMSTATE_UINT32(del, BCM2835I2CState), VMSTATE_UINT32(clkt, BCM2835I2CState), VMSTATE_UINT32(last_dlen, BCM2835I2CState), VMSTATE_END_OF_LIST() } }; static void bcm2835_i2c_class_init(ObjectClass *klass, void *data) { DeviceClass *dc = DEVICE_CLASS(klass); device_class_set_legacy_reset(dc, bcm2835_i2c_reset); dc->realize = bcm2835_i2c_realize; dc->vmsd = &vmstate_bcm2835_i2c; } static const TypeInfo bcm2835_i2c_info = { .name = TYPE_BCM2835_I2C, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(BCM2835I2CState), .class_init = bcm2835_i2c_class_init, }; static void bcm2835_i2c_register_types(void) { type_register_static(&bcm2835_i2c_info); } type_init(bcm2835_i2c_register_types)