/* * 86Box A hypervisor and IBM PC system emulator that specializes in * running old operating systems and software designed for IBM * PC systems and compatibles from 1981 through fairly recent * system designs based on the PCI bus. * * This file is part of the 86Box distribution. * * Implementation of a generic ALi M7101-compatible SMBus host * controller. * * Authors: RichardG, * Miran Grca, * * Copyright 2020-2021 RichardG. * Copyright 2021 Miran Grca. */ #include #include #include #include #include #include #define HAVE_STDARG_H #include <86box/86box.h> #include <86box/io.h> #include <86box/device.h> #include <86box/timer.h> #include <86box/i2c.h> #include <86box/smbus.h> #include <86box/plat_fallthrough.h> #ifdef ENABLE_SMBUS_ALI7101_LOG int smbus_ali7101_do_log = ENABLE_SMBUS_ALI7101_LOG; static void smbus_ali7101_log(const char *fmt, ...) { va_list ap; if (smbus_ali7101_do_log) { va_start(ap, fmt); pclog_ex(fmt, ap); va_end(ap); } } #else # define smbus_ali7101_log(fmt, ...) #endif static uint8_t smbus_ali7101_read(uint16_t addr, void *priv) { smbus_ali7101_t *dev = (smbus_ali7101_t *) priv; uint8_t ret = 0x00; switch (addr - dev->io_base) { case 0x00: ret = dev->stat; break; case 0x03: ret = dev->addr; break; case 0x04: ret = dev->data0; break; case 0x05: ret = dev->data1; break; case 0x06: ret = dev->data[dev->index++]; if (dev->index >= SMBUS_ALI7101_BLOCK_DATA_SIZE) dev->index = 0; break; case 0x07: ret = dev->cmd; break; default: break; } smbus_ali7101_log("SMBus ALI7101: read(%02X) = %02x\n", addr, ret); return ret; } static void smbus_ali7101_write(uint16_t addr, uint8_t val, void *priv) { smbus_ali7101_t *dev = (smbus_ali7101_t *) priv; uint8_t smbus_addr; uint8_t cmd; uint8_t read; uint8_t prev_stat; uint16_t timer_bytes = 0; smbus_ali7101_log("SMBus ALI7101: write(%02X, %02X)\n", addr, val); prev_stat = dev->next_stat; dev->next_stat = 0x04; switch (addr - dev->io_base) { case 0x00: dev->stat &= ~(val & 0xf2); /* Make sure IDLE is set if we're not busy or errored. */ if (dev->stat == 0x00) dev->stat = 0x04; break; case 0x01: dev->ctl = val & 0xfc; if (val & 0x04) { /* cancel an in-progress command if KILL is set */ if (prev_stat) { /* cancel only if a command is in progress */ timer_disable(&dev->response_timer); dev->stat = 0x80; /* raise FAILED */ } } else if (val & 0x08) { /* T_OUT_CMD */ if (prev_stat) { /* cancel only if a command is in progress */ timer_disable(&dev->response_timer); dev->stat = 0x20; /* raise DEVICE_ERR */ } } if (val & 0x80) dev->index = 0; break; case 0x02: /* dispatch command if START is set */ timer_bytes++; /* address */ smbus_addr = (dev->addr >> 1); read = dev->addr & 0x01; cmd = (dev->ctl >> 4) & 0x7; smbus_ali7101_log("SMBus ALI7101: addr=%02X read=%d protocol=%X cmd=%02X data0=%02X data1=%02X\n", smbus_addr, read, cmd, dev->cmd, dev->data0, dev->data1); /* Raise DEV_ERR if no device is at this address, or if the device returned NAK when starting the transfer. */ if (!i2c_start(i2c_smbus, smbus_addr, read)) { dev->next_stat = 0x40; break; } dev->next_stat = 0x10; /* raise INTER (command completed) by default */ /* Decode the command protocol. */ switch (cmd) { case 0x0: /* quick R/W */ break; case 0x1: /* byte R/W */ if (read) /* byte read */ dev->data0 = i2c_read(i2c_smbus, smbus_addr); else /* byte write */ i2c_write(i2c_smbus, smbus_addr, dev->data0); timer_bytes++; break; case 0x2: /* byte data R/W */ /* command write */ i2c_write(i2c_smbus, smbus_addr, dev->cmd); timer_bytes++; if (read) /* byte read */ dev->data0 = i2c_read(i2c_smbus, smbus_addr); else /* byte write */ i2c_write(i2c_smbus, smbus_addr, dev->data0); timer_bytes++; break; case 0x3: /* word data R/W */ /* command write */ i2c_write(i2c_smbus, smbus_addr, dev->cmd); timer_bytes++; if (read) { /* word read */ dev->data0 = i2c_read(i2c_smbus, smbus_addr); dev->data1 = i2c_read(i2c_smbus, smbus_addr); } else { /* word write */ i2c_write(i2c_smbus, smbus_addr, dev->data0); i2c_write(i2c_smbus, smbus_addr, dev->data1); } timer_bytes += 2; break; case 0x4: /* block R/W */ timer_bytes++; /* count the SMBus length byte now */ fallthrough; default: /* unknown */ dev->next_stat = 0x20; /* raise DEV_ERR */ timer_bytes = 0; break; } /* Finish transfer. */ i2c_stop(i2c_smbus, smbus_addr); break; case 0x03: dev->addr = val; break; case 0x04: dev->data0 = val; break; case 0x05: dev->data1 = val; break; case 0x06: dev->data[dev->index++] = val; if (dev->index >= SMBUS_ALI7101_BLOCK_DATA_SIZE) dev->index = 0; break; case 0x07: dev->cmd = val; break; default: break; } if (dev->next_stat != 0x04) { /* schedule dispatch of any pending status register update */ dev->stat = 0x08; /* raise HOST_BUSY while waiting */ timer_disable(&dev->response_timer); /* delay = ((half clock for start + half clock for stop) + (bytes * (8 bits + ack))) * 60us period measured on real VIA 686B */ timer_set_delay_u64(&dev->response_timer, (1 + (timer_bytes * 9)) * 60 * TIMER_USEC); } } static void smbus_ali7101_response(void *priv) { smbus_ali7101_t *dev = (smbus_ali7101_t *) priv; /* Dispatch the status register update. */ dev->stat = dev->next_stat; } void smbus_ali7101_remap(smbus_ali7101_t *dev, uint16_t new_io_base, uint8_t enable) { if (dev->io_base) io_removehandler(dev->io_base, 0x10, smbus_ali7101_read, NULL, NULL, smbus_ali7101_write, NULL, NULL, dev); dev->io_base = new_io_base; smbus_ali7101_log("SMBus ALI7101: remap to %04Xh (%sabled)\n", dev->io_base, enable ? "en" : "dis"); if (enable && dev->io_base) io_sethandler(dev->io_base, 0x10, smbus_ali7101_read, NULL, NULL, smbus_ali7101_write, NULL, NULL, dev); } static void smbus_ali7101_reset(void *priv) { smbus_ali7101_t *dev = (smbus_ali7101_t *) priv; timer_disable(&dev->response_timer); dev->stat = 0x04; } static void * smbus_ali7101_init(const device_t *info) { smbus_ali7101_t *dev = (smbus_ali7101_t *) malloc(sizeof(smbus_ali7101_t)); memset(dev, 0, sizeof(smbus_ali7101_t)); dev->local = info->local; dev->stat = 0x04; /* We save the I2C bus handle on dev but use i2c_smbus for all operations because dev and therefore dev->i2c will be invalidated if a device triggers a hard reset. */ i2c_smbus = dev->i2c = i2c_addbus("smbus_ali7101"); timer_add(&dev->response_timer, smbus_ali7101_response, dev, 0); return dev; } static void smbus_ali7101_close(void *priv) { smbus_ali7101_t *dev = (smbus_ali7101_t *) priv; if (i2c_smbus == dev->i2c) i2c_smbus = NULL; i2c_removebus(dev->i2c); free(dev); } const device_t ali7101_smbus_device = { .name = "ALi M7101-compatible SMBus Host Controller", .internal_name = "ali7101_smbus", .flags = DEVICE_AT, .local = 0, .init = smbus_ali7101_init, .close = smbus_ali7101_close, .reset = smbus_ali7101_reset, { .available = NULL }, .speed_changed = NULL, .force_redraw = NULL, .config = NULL };