/* SPDX-License-Identifier: GPL-2.0-or-later */ #include <cpu/power/scom.h> #include <cpu/power/spr.h> // HMER #include <console/console.h> #define XSCOM_ADDR_IND_ADDR PPC_BITMASK(11, 31) #define XSCOM_ADDR_IND_DATA PPC_BITMASK(48, 63) #define XSCOM_DATA_IND_READ PPC_BIT(0) #define XSCOM_DATA_IND_COMPLETE PPC_BIT(32) #define XSCOM_DATA_IND_ERR PPC_BITMASK(33, 35) #define XSCOM_DATA_IND_DATA PPC_BITMASK(48, 63) #define XSCOM_DATA_IND_FORM1_DATA PPC_BITMASK(12, 63) #define XSCOM_IND_MAX_RETRIES 10 #define XSCOM_RCVED_STAT_REG 0x00090018 #define XSCOM_LOG_REG 0x00090012 #define XSCOM_ERR_REG 0x00090013 static void reset_scom_engine(void) { /* * With cross-CPU SCOM accesses, first register should be cleared on the * executing CPU, the other two on target CPU. In that case it may be * necessary to do the remote writes in assembly directly to skip checking * HMER and possibly end in a loop. */ write_scom_direct(XSCOM_RCVED_STAT_REG, 0); write_scom_direct(XSCOM_LOG_REG, 0); write_scom_direct(XSCOM_ERR_REG, 0); clear_hmer(); eieio(); } uint64_t read_scom_direct(uint64_t reg_address) { uint64_t val; uint64_t hmer = 0; do { /* * Clearing HMER on every SCOM access seems to slow down CCS up * to a point where it starts hitting timeout on "less ideal" * DIMMs for write centering. Clear it only if this do...while * executes more than once. */ if ((hmer & SPR_HMER_XSCOM_STATUS) == SPR_HMER_XSCOM_OCCUPIED) clear_hmer(); eieio(); asm volatile( "ldcix %0, %1, %2" : "=r"(val) : "b"(MMIO_GROUP0_CHIP0_SCOM_BASE_ADDR), "r"(reg_address << 3)); eieio(); hmer = read_hmer(); } while ((hmer & SPR_HMER_XSCOM_STATUS) == SPR_HMER_XSCOM_OCCUPIED); if (hmer & SPR_HMER_XSCOM_STATUS) { reset_scom_engine(); /* * All F's are returned in case of error, but code polls for a set bit * after changes that can make such error appear (e.g. clock settings). * Return 0 so caller won't have to test for all F's in that case. */ return 0; } return val; } void write_scom_direct(uint64_t reg_address, uint64_t data) { uint64_t hmer = 0; do { /* See comment in read_scom_direct() */ if ((hmer & SPR_HMER_XSCOM_STATUS) == SPR_HMER_XSCOM_OCCUPIED) clear_hmer(); eieio(); asm volatile( "stdcix %0, %1, %2":: "r"(data), "b"(MMIO_GROUP0_CHIP0_SCOM_BASE_ADDR), "r"(reg_address << 3)); eieio(); hmer = read_hmer(); } while ((hmer & SPR_HMER_XSCOM_STATUS) == SPR_HMER_XSCOM_OCCUPIED); if (hmer & SPR_HMER_XSCOM_STATUS) reset_scom_engine(); } void write_scom_indirect(uint64_t reg_address, uint64_t value) { uint64_t addr; uint64_t data; addr = reg_address & 0x7FFFFFFF; data = reg_address & XSCOM_ADDR_IND_ADDR; data |= value & XSCOM_ADDR_IND_DATA; write_scom_direct(addr, data); for (int retries = 0; retries < XSCOM_IND_MAX_RETRIES; ++retries) { data = read_scom_direct(addr); if ((data & XSCOM_DATA_IND_COMPLETE) && ((data & XSCOM_DATA_IND_ERR) == 0)) { return; } else if (data & XSCOM_DATA_IND_COMPLETE) { printk(BIOS_EMERG, "SCOM WR error %16.16llx = %16.16llx : %16.16llx\n", reg_address, value, data); } // TODO: delay? } } uint64_t read_scom_indirect(uint64_t reg_address) { uint64_t addr; uint64_t data; addr = reg_address & 0x7FFFFFFF; data = XSCOM_DATA_IND_READ | (reg_address & XSCOM_ADDR_IND_ADDR); write_scom_direct(addr, data); for (int retries = 0; retries < XSCOM_IND_MAX_RETRIES; ++retries) { data = read_scom_direct(addr); if ((data & XSCOM_DATA_IND_COMPLETE) && ((data & XSCOM_DATA_IND_ERR) == 0)) { break; } else if (data & XSCOM_DATA_IND_COMPLETE) { printk(BIOS_EMERG, "SCOM RD error %16.16llx : %16.16llx\n", reg_address, data); } // TODO: delay? } return data & XSCOM_DATA_IND_DATA; }