| /* 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_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 |
| |
| 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; |
| } |
| |
| /* This function should be rarely called, don't make it inlined */ |
| 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(); |
| } |