/* * Copyright (c) 2022 Intel Corporation * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include LOG_MODULE_REGISTER(emul, LOG_LEVEL_DBG); #include "emul.h" /** * Emulate Intel PCH Host Controller hardware as PCI device with I/O access */ /* PCI Configuration space */ uint32_t pci_config_area[32] = { [PCIE_CONF_CMDSTAT] = PCIE_CONF_CMDSTAT_INTERRUPT, /* Mark INT */ [8] = 1, /* I/O BAR */ [16] = 1, /* Enable SMBus */ }; /* I/O and MMIO registers */ uint8_t io_area[24] = { 0, }; struct e32_block { uint8_t buf[SMBUS_BLOCK_BYTES_MAX]; uint8_t offset; } e32; /** * Emulate SMBus peripheral device, connected to the bus as * simple EEPROM device of size 256 bytes */ /* List of peripheral devices registered */ sys_slist_t peripherals; void emul_register_smbus_peripheral(struct smbus_peripheral *peripheral) { sys_slist_prepend(&peripherals, &peripheral->node); } static struct smbus_peripheral *emul_get_smbus_peripheral(uint8_t addr) { struct smbus_peripheral *peripheral, *tmp; SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&peripherals, peripheral, tmp, node) { if (peripheral->addr == addr) { return peripheral; } } return NULL; } static bool peripheral_handle_smbalert(void) { struct smbus_peripheral *peripheral, *tmp, *found = NULL; SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&peripherals, peripheral, tmp, node) { if (peripheral->smbalert && !peripheral->smbalert_handled) { found = peripheral; } } if (found == NULL) { LOG_WRN("No (more) smbalert handlers found"); return false; } LOG_DBG("Return own address: 0x%02x", found->addr); io_area[PCH_SMBUS_HD0] = found->addr; found->smbalert_handled = true; return true; } bool peripheral_handle_host_notify(void) { struct smbus_peripheral *peripheral, *tmp; SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&peripherals, peripheral, tmp, node) { if (peripheral->host_notify) { LOG_DBG("Save own peripheral address to NDA"); io_area[PCH_SMBUS_NDA] = peripheral->addr << 1; return true; } } return false; } static void peripheral_write(uint8_t reg, uint8_t value) { uint8_t addr = PCH_SMBUS_TSA_ADDR_GET(io_area[PCH_SMBUS_TSA]); struct smbus_peripheral *peripheral; peripheral = emul_get_smbus_peripheral(addr); if (peripheral) { peripheral->raw_data[reg] = value; LOG_DBG("peripheral: [0x%02x] <= 0x%02x", reg, value); } else { LOG_ERR("Peripheral not found, addr 0x%02x", addr); } } static void peripheral_read(uint8_t reg, uint8_t *value) { uint8_t addr = PCH_SMBUS_TSA_ADDR_GET(io_area[PCH_SMBUS_TSA]); struct smbus_peripheral *peripheral; peripheral = emul_get_smbus_peripheral(addr); if (peripheral) { *value = peripheral->raw_data[reg]; LOG_DBG("peripheral: [0x%02x] => 0x%02x", reg, *value); } else { LOG_ERR("Peripheral not found, addr 0x%02x", addr); } } static void emul_start_smbus_protocol(void) { uint8_t smbus_cmd = PCH_SMBUS_HCTL_CMD_GET(io_area[PCH_SMBUS_HCTL]); bool write = (io_area[PCH_SMBUS_TSA] & PCH_SMBUS_TSA_RW) == SMBUS_MSG_WRITE; uint8_t addr = PCH_SMBUS_TSA_ADDR_GET(io_area[PCH_SMBUS_TSA]); struct smbus_peripheral *peripheral; LOG_DBG("Start SMBUS protocol"); if (unlikely(addr == SMBUS_ADDRESS_ARA)) { if (peripheral_handle_smbalert()) { goto isr; } } peripheral = emul_get_smbus_peripheral(addr); if (peripheral == NULL) { LOG_WRN("Set Device Error"); emul_set_io(emul_get_io(PCH_SMBUS_HSTS) | PCH_SMBUS_HSTS_DEV_ERROR, PCH_SMBUS_HSTS); goto isr; } switch (smbus_cmd) { case PCH_SMBUS_HCTL_CMD_QUICK: LOG_DBG("Quick command"); break; case PCH_SMBUS_HCTL_CMD_BYTE: if (write) { LOG_DBG("Byte Write command"); peripheral_write(0, io_area[PCH_SMBUS_HCMD]); } else { LOG_DBG("Byte Read command"); peripheral_read(0, &io_area[PCH_SMBUS_HD0]); } break; case PCH_SMBUS_HCTL_CMD_BYTE_DATA: if (write) { LOG_DBG("Byte Data Write command"); peripheral_write(io_area[PCH_SMBUS_HCMD], io_area[PCH_SMBUS_HD0]); } else { LOG_DBG("Byte Data Read command"); peripheral_read(io_area[PCH_SMBUS_HCMD], &io_area[PCH_SMBUS_HD0]); } break; case PCH_SMBUS_HCTL_CMD_WORD_DATA: if (write) { LOG_DBG("Word Data Write command"); peripheral_write(io_area[PCH_SMBUS_HCMD], io_area[PCH_SMBUS_HD0]); peripheral_write(io_area[PCH_SMBUS_HCMD] + 1, io_area[PCH_SMBUS_HD1]); } else { LOG_DBG("Word Data Read command"); peripheral_read(io_area[PCH_SMBUS_HCMD], &io_area[PCH_SMBUS_HD0]); peripheral_read(io_area[PCH_SMBUS_HCMD] + 1, &io_area[PCH_SMBUS_HD1]); } break; case PCH_SMBUS_HCTL_CMD_PROC_CALL: if (!write) { LOG_ERR("Incorrect operation flag"); return; } LOG_DBG("Process Call command"); peripheral_write(io_area[PCH_SMBUS_HCMD], io_area[PCH_SMBUS_HD0]); peripheral_write(io_area[PCH_SMBUS_HCMD] + 1, io_area[PCH_SMBUS_HD1]); /** * For the testing purposes implement data * swap for the Proc Call, that would be * easy for testing. * * Note: real device should have some other * logic for Proc Call. */ peripheral_read(io_area[PCH_SMBUS_HCMD], &io_area[PCH_SMBUS_HD1]); peripheral_read(io_area[PCH_SMBUS_HCMD] + 1, &io_area[PCH_SMBUS_HD0]); break; case PCH_SMBUS_HCTL_CMD_BLOCK: if (write) { uint8_t count = io_area[PCH_SMBUS_HD0]; uint8_t reg = io_area[PCH_SMBUS_HCMD]; LOG_DBG("Block Write command"); if (count > SMBUS_BLOCK_BYTES_MAX) { return; } for (int i = 0; i < count; i++) { peripheral_write(reg++, e32.buf[i]); } } else { /** * count is set by peripheral device, just * assume it to be maximum block count */ uint8_t count = SMBUS_BLOCK_BYTES_MAX; uint8_t reg = io_area[PCH_SMBUS_HCMD]; LOG_DBG("Block Read command"); for (int i = 0; i < count; i++) { peripheral_read(reg++, &e32.buf[i]); } /* Set count */ io_area[PCH_SMBUS_HD0] = count; } break; case PCH_SMBUS_HCTL_CMD_BLOCK_PROC: if (!write) { LOG_ERR("Incorrect operation flag"); } else { uint8_t snd_count = io_area[PCH_SMBUS_HD0]; uint8_t reg = io_area[PCH_SMBUS_HCMD]; uint8_t rcv_count; LOG_DBG("Block Process Call command"); if (snd_count > SMBUS_BLOCK_BYTES_MAX) { return; } /** * Make Block Process Call swap block buffer bytes * for testing purposes only, return the same "count" * bytes */ for (int i = 0; i < snd_count; i++) { peripheral_write(reg++, e32.buf[i]); } rcv_count = snd_count; if (snd_count + rcv_count > SMBUS_BLOCK_BYTES_MAX) { return; } for (int i = 0; i < rcv_count; i++) { peripheral_read(--reg, &e32.buf[i]); } /* Clear offset count */ e32.offset = 0; /* Set count */ io_area[PCH_SMBUS_HD0] = rcv_count; } break; default: LOG_ERR("Protocol is not implemented yet in emul"); break; } isr: if (io_area[PCH_SMBUS_HCTL] & PCH_SMBUS_HCTL_INTR_EN) { /* Fire emulated interrupt if enabled */ run_isr(EMUL_SMBUS_INTR); } } static void emul_evaluate_write(uint8_t value, io_port_t addr) { switch (addr) { case PCH_SMBUS_HCTL: if (value & PCH_SMBUS_HCTL_START) { /* This is write only */ io_area[PCH_SMBUS_HCTL] = value & ~PCH_SMBUS_HCTL_START; emul_start_smbus_protocol(); } break; default: break; } } static const char *pch_get_reg_name(uint8_t reg) { switch (reg) { case PCH_SMBUS_HSTS: return "HSTS"; case PCH_SMBUS_HCTL: return "HCTL"; case PCH_SMBUS_HCMD: return "HCMD"; case PCH_SMBUS_TSA: return "TSA"; case PCH_SMBUS_HD0: return "HD0"; case PCH_SMBUS_HD1: return "HD1"; case PCH_SMBUS_HBD: return "HBD"; case PCH_SMBUS_PEC: return "PEC"; case PCH_SMBUS_RSA: return "RSA"; case PCH_SMBUS_SD: return "SD"; case PCH_SMBUS_AUXS: return "AUXS"; case PCH_SMBUS_AUXC: return "AUXC"; case PCH_SMBUS_SMLC: return "SMLC"; case PCH_SMBUS_SMBC: return "SMBC"; case PCH_SMBUS_SSTS: return "SSTS"; case PCH_SMBUS_SCMD: return "SCMD"; case PCH_SMBUS_NDA: return "NDA"; case PCH_SMBUS_NDLB: return "NDLB"; case PCH_SMBUS_NDHB: return "NDHB"; default: return "Unknown"; } } uint32_t emul_pci_read(unsigned int reg) { LOG_DBG("PCI [%x] => 0x%x", reg, pci_config_area[reg]); return pci_config_area[reg]; } void emul_pci_write(pcie_bdf_t bdf, unsigned int reg, uint32_t value) { LOG_DBG("PCI [%x] <= 0x%x", reg, value); pci_config_area[reg] = value; } /* This function is used to set registers for emulation purpose */ void emul_set_io(uint8_t value, io_port_t addr) { io_area[addr] = value; } uint8_t emul_get_io(io_port_t addr) { return io_area[addr]; } void emul_out8(uint8_t value, io_port_t addr) { switch (addr) { case PCH_SMBUS_HSTS: /* Writing clears status bits */ io_area[addr] &= ~value; break; case PCH_SMBUS_SSTS: /* Writing clears status bits */ io_area[addr] &= ~value; break; case PCH_SMBUS_HBD: /* Using internal E32 buffer offset */ e32.buf[e32.offset++] = value; break; case PCH_SMBUS_AUXC: if (value & PCH_SMBUS_AUXC_EN_32BUF) { LOG_DBG("Enabled 32 bit buffer block mode"); } io_area[addr] = value; break; default: io_area[addr] = value; break; } LOG_DBG("I/O [%s] <= 0x%x => 0x%x", pch_get_reg_name(addr), value, io_area[addr]); /** * Evaluate should decide about starting actual SMBus * protocol transaction emulation */ emul_evaluate_write(value, addr); } uint8_t emul_in8(io_port_t addr) { uint8_t value; switch (addr) { case PCH_SMBUS_HBD: /* Using internal E32 buffer offset */ value = e32.buf[e32.offset++]; break; case PCH_SMBUS_HCTL: /* Clear e32 block buffer offset */ e32.offset = 0; LOG_WRN("E32 buffer offset is cleared"); value = io_area[addr]; break; default: value = io_area[addr]; break; } LOG_DBG("I/O [%s] => 0x%x", pch_get_reg_name(addr), value); return value; }