drm/amd/display: Workaround for stuck I2C arbitrage

[Why]
When booting without an HDMI display connected, the I2C registers
are not initialized correctly, leading to DC_I2C_ARBITRATION register
getting stuck with DC_I2C_REG_RW_CNTL_STATUS == USED_BY_SW.

[How]
* Correct TOCTOU race condition in engine acquire logic which did not
check against DMUB trying to acquire it at the same time.
* Deassert SOFT_RESET before acquire, as it can block access to other
I2C registers.
* Add a workaround in release, checking that after triggerring
DC_I2C_SW_DONE_USING_I2C_REG, DC_I2C_REG_RW_CNTL_STATUS != USED_BY_SW.
If necessary, trigger DC_I2C_SW_DONE_USING_I2C_REG again.
* Remove unnecessary clear of DC_I2C_SW_USE_I2C_REG_REQ, which engine
ignores according to specification.

Reviewed-by: Alvin Lee <alvin.lee2@amd.com>
Signed-off-by: Dominik Kaszewski <dominik.kaszewski@amd.com>
Signed-off-by: Ivan Lipski <ivan.lipski@amd.com>
Tested-by: Daniel Wheeler <daniel.wheeler@amd.com>
Signed-off-by: Alex Deucher <alexander.deucher@amd.com>
This commit is contained in:
Dominik Kaszewski 2025-06-24 12:40:25 +02:00 committed by Alex Deucher
parent 48cb9c3b21
commit 04d57f4462

View File

@ -292,9 +292,35 @@ static void set_speed(
FN(DC_I2C_DDC1_SPEED, DC_I2C_DDC1_THRESHOLD), 2);
}
static bool acquire_engine(struct dce_i2c_hw *dce_i2c_hw)
{
uint32_t arbitrate = 0;
REG_GET(DC_I2C_ARBITRATION, DC_I2C_REG_RW_CNTL_STATUS, &arbitrate);
switch (arbitrate) {
case DC_I2C_STATUS__DC_I2C_STATUS_USED_BY_SW:
return true;
case DC_I2C_STATUS__DC_I2C_STATUS_USED_BY_HW:
return false;
case DC_I2C_STATUS__DC_I2C_STATUS_IDLE:
default:
break;
}
REG_UPDATE(DC_I2C_ARBITRATION, DC_I2C_SW_USE_I2C_REG_REQ, true);
REG_GET(DC_I2C_ARBITRATION, DC_I2C_REG_RW_CNTL_STATUS, &arbitrate);
if (arbitrate != DC_I2C_STATUS__DC_I2C_STATUS_USED_BY_SW)
return false;
return true;
}
static bool setup_engine(
struct dce_i2c_hw *dce_i2c_hw)
{
// Deassert soft reset to unblock I2C engine registers
REG_UPDATE(DC_I2C_CONTROL, DC_I2C_SOFT_RESET, false);
uint32_t i2c_setup_limit = I2C_SETUP_TIME_LIMIT_DCE;
uint32_t reset_length = 0;
@ -309,8 +335,8 @@ static bool setup_engine(
REG_UPDATE_N(SETUP, 1,
FN(DC_I2C_DDC1_SETUP, DC_I2C_DDC1_CLK_EN), 1);
/* we have checked I2c not used by DMCU, set SW use I2C REQ to 1 to indicate SW using it*/
REG_UPDATE(DC_I2C_ARBITRATION, DC_I2C_SW_USE_I2C_REG_REQ, 1);
if (!acquire_engine(dce_i2c_hw))
return false;
/*set SW requested I2c speed to default, if API calls in it will be override later*/
set_speed(dce_i2c_hw, dce_i2c_hw->ctx->dc->caps.i2c_speed_in_khz);
@ -319,9 +345,8 @@ static bool setup_engine(
i2c_setup_limit = dce_i2c_hw->setup_limit;
/* Program pin select */
REG_UPDATE_6(DC_I2C_CONTROL,
REG_UPDATE_5(DC_I2C_CONTROL,
DC_I2C_GO, 0,
DC_I2C_SOFT_RESET, 0,
DC_I2C_SEND_RESET, 0,
DC_I2C_SW_STATUS_RESET, 1,
DC_I2C_TRANSACTION_COUNT, 0,
@ -351,6 +376,26 @@ static bool setup_engine(
return true;
}
/**
* If we boot without an HDMI display, the I2C engine does not get initialized
* correctly. One of its symptoms is that SW_USE_I2C does not get cleared after
* acquire, so that after setting SW_DONE_USING_I2C on release, the engine gets
* immediately reacquired by SW, preventing DMUB from using it.
*/
static void cntl_stuck_hw_workaround(struct dce_i2c_hw *dce_i2c_hw)
{
uint32_t arbitrate = 0;
REG_GET(DC_I2C_ARBITRATION, DC_I2C_REG_RW_CNTL_STATUS, &arbitrate);
if (arbitrate != DC_I2C_STATUS__DC_I2C_STATUS_USED_BY_SW)
return;
// Still acquired after release, release again as a workaround
REG_UPDATE(DC_I2C_ARBITRATION, DC_I2C_SW_DONE_USING_I2C_REG, true);
REG_GET(DC_I2C_ARBITRATION, DC_I2C_REG_RW_CNTL_STATUS, &arbitrate);
ASSERT(arbitrate != DC_I2C_STATUS__DC_I2C_STATUS_USED_BY_SW);
}
static void release_engine(
struct dce_i2c_hw *dce_i2c_hw)
{
@ -378,9 +423,9 @@ static void release_engine(
/*for HW HDCP Ri polling failure w/a test*/
set_speed(dce_i2c_hw, dce_i2c_hw->ctx->dc->caps.i2c_speed_in_khz_hdcp);
/* Release I2C after reset, so HW or DMCU could use it */
REG_UPDATE_2(DC_I2C_ARBITRATION, DC_I2C_SW_DONE_USING_I2C_REG, 1,
DC_I2C_SW_USE_I2C_REG_REQ, 0);
// Release I2C engine so it can be used by HW or DMCU, automatically clears SW_USE_I2C
REG_UPDATE(DC_I2C_ARBITRATION, DC_I2C_SW_DONE_USING_I2C_REG, true);
cntl_stuck_hw_workaround(dce_i2c_hw);
if (dce_i2c_hw->ctx->dc->debug.enable_mem_low_power.bits.i2c) {
if (dce_i2c_hw->regs->DIO_MEM_PWR_CTRL)