drm/amd/display: Add KUnit tests for HDCP DDC link adapters

Expose the mod_hdcp DDC adapter callbacks for KUnit and add tests:

lp_write_i2c / lp_read_i2c / lp_write_dpcd / lp_read_dpcd use recording
fake i2c and DP aux backends to assert the built payloads (write flag,
address, length, buffer, and the read offset-then-data sequence), plus
failure when the connector is missing.

lp_atomic_write_poll_read_i2c / lp_atomic_write_poll_read_aux cover the
hardware-free early returns (NULL link and a payload too large to convert);
the success path submits fused-IO to the DMCUB and is out of reach for a
unit test.

Reviewed-by: Alex Hung <alex.hung@amd.com>
Signed-off-by: Bhawanpreet Lakha <bhawanpreet.lakha@amd.com>
Signed-off-by: Fangzhi Zuo <jerry.zuo@amd.com>
Tested-by: Dan Wheeler <daniel.wheeler@amd.com>
Signed-off-by: Alex Deucher <alexander.deucher@amd.com>
This commit is contained in:
Bhawanpreet Lakha 2026-07-14 12:37:25 -04:00 committed by Alex Deucher
parent eb5e911dff
commit 249401c325
3 changed files with 418 additions and 22 deletions

View File

@ -38,8 +38,7 @@
*/
#define PSP_SRM_VERSION_MAX 0xFFFF
static bool
lp_write_i2c(void *handle, uint32_t address, const uint8_t *data, uint32_t size)
STATIC_IFN_KUNIT bool lp_write_i2c(void *handle, uint32_t address, const uint8_t *data, uint32_t size)
{
struct dc_link *link = handle;
struct i2c_payload i2c_payloads[] = {{true, address, size, (void *)data} };
@ -48,9 +47,9 @@ lp_write_i2c(void *handle, uint32_t address, const uint8_t *data, uint32_t size)
return dm_helpers_submit_i2c(link->ctx, link, &cmd);
}
EXPORT_IF_KUNIT(lp_write_i2c);
static bool
lp_read_i2c(void *handle, uint32_t address, uint8_t offset, uint8_t *data, uint32_t size)
STATIC_IFN_KUNIT bool lp_read_i2c(void *handle, uint32_t address, uint8_t offset, uint8_t *data, uint32_t size)
{
struct dc_link *link = handle;
@ -61,50 +60,53 @@ lp_read_i2c(void *handle, uint32_t address, uint8_t offset, uint8_t *data, uint3
return dm_helpers_submit_i2c(link->ctx, link, &cmd);
}
EXPORT_IF_KUNIT(lp_read_i2c);
static bool
lp_write_dpcd(void *handle, uint32_t address, const uint8_t *data, uint32_t size)
STATIC_IFN_KUNIT bool lp_write_dpcd(void *handle, uint32_t address, const uint8_t *data, uint32_t size)
{
struct dc_link *link = handle;
return dm_helpers_dp_write_dpcd(link->ctx, link, address, data, size);
}
EXPORT_IF_KUNIT(lp_write_dpcd);
static bool
lp_read_dpcd(void *handle, uint32_t address, uint8_t *data, uint32_t size)
STATIC_IFN_KUNIT bool lp_read_dpcd(void *handle, uint32_t address, uint8_t *data, uint32_t size)
{
struct dc_link *link = handle;
return dm_helpers_dp_read_dpcd(link->ctx, link, address, data, size);
}
EXPORT_IF_KUNIT(lp_read_dpcd);
static bool lp_atomic_write_poll_read_i2c(
void *handle,
const struct mod_hdcp_atomic_op_i2c *write,
const struct mod_hdcp_atomic_op_i2c *poll,
struct mod_hdcp_atomic_op_i2c *read,
uint32_t poll_timeout_us,
uint8_t poll_mask_msb
STATIC_IFN_KUNIT bool lp_atomic_write_poll_read_i2c(
void *handle,
const struct mod_hdcp_atomic_op_i2c *write,
const struct mod_hdcp_atomic_op_i2c *poll,
struct mod_hdcp_atomic_op_i2c *read,
uint32_t poll_timeout_us,
uint8_t poll_mask_msb
)
{
struct dc_link *link = handle;
return dm_atomic_write_poll_read_i2c(link, write, poll, read, poll_timeout_us, poll_mask_msb);
}
EXPORT_IF_KUNIT(lp_atomic_write_poll_read_i2c);
static bool lp_atomic_write_poll_read_aux(
void *handle,
const struct mod_hdcp_atomic_op_aux *write,
const struct mod_hdcp_atomic_op_aux *poll,
struct mod_hdcp_atomic_op_aux *read,
uint32_t poll_timeout_us,
uint8_t poll_mask_msb
STATIC_IFN_KUNIT bool lp_atomic_write_poll_read_aux(
void *handle,
const struct mod_hdcp_atomic_op_aux *write,
const struct mod_hdcp_atomic_op_aux *poll,
struct mod_hdcp_atomic_op_aux *read,
uint32_t poll_timeout_us,
uint8_t poll_mask_msb
)
{
struct dc_link *link = handle;
return dm_atomic_write_poll_read_aux(link, write, poll, read, poll_timeout_us, poll_mask_msb);
}
EXPORT_IF_KUNIT(lp_atomic_write_poll_read_aux);
STATIC_IFN_KUNIT
uint8_t *psp_get_srm(struct psp_context *psp, uint32_t *srm_version, uint32_t *srm_size)

View File

@ -48,6 +48,8 @@ struct psp_context;
struct file;
struct kobject;
struct bin_attribute;
struct mod_hdcp_atomic_op_i2c;
struct mod_hdcp_atomic_op_aux;
struct hdcp_workqueue {
struct work_struct cpirq_work;
@ -134,6 +136,20 @@ ssize_t srm_data_write(struct file *filp, struct kobject *kobj,
ssize_t srm_data_read(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr, char *buffer,
loff_t pos, size_t count);
bool lp_write_i2c(void *handle, uint32_t address, const uint8_t *data, uint32_t size);
bool lp_read_i2c(void *handle, uint32_t address, uint8_t offset, uint8_t *data, uint32_t size);
bool lp_write_dpcd(void *handle, uint32_t address, const uint8_t *data, uint32_t size);
bool lp_read_dpcd(void *handle, uint32_t address, uint8_t *data, uint32_t size);
bool lp_atomic_write_poll_read_i2c(void *handle,
const struct mod_hdcp_atomic_op_i2c *write,
const struct mod_hdcp_atomic_op_i2c *poll,
struct mod_hdcp_atomic_op_i2c *read,
uint32_t poll_timeout_us, uint8_t poll_mask_msb);
bool lp_atomic_write_poll_read_aux(void *handle,
const struct mod_hdcp_atomic_op_aux *write,
const struct mod_hdcp_atomic_op_aux *poll,
struct mod_hdcp_atomic_op_aux *read,
uint32_t poll_timeout_us, uint8_t poll_mask_msb);
#endif
#endif /* AMDGPU_DM_AMDGPU_DM_HDCP_H_ */

View File

@ -10,6 +10,9 @@
#include <linux/kobject.h>
#include <linux/slab.h>
#include <linux/sysfs.h>
#include <linux/i2c.h>
#include <drm/display/drm_dp_helper.h>
#include "amdgpu.h"
#include "amdgpu_dm.h"
@ -1461,6 +1464,369 @@ static void dm_test_srm_data_read_empty_srm_returns_zero(struct kunit *test)
/* End of tests for srm_data_write() and srm_data_read() */
/* Tests for lp_write_i2c() / lp_read_i2c() / lp_write_dpcd() / lp_read_dpcd() */
/* Defined further below with the display helper tests. */
static struct amdgpu_dm_connector *alloc_test_connector(struct kunit *test,
unsigned int index);
/*
* Recording fakes for the DDC layer. The lp_* wrappers build i2c/DPCD
* transactions and forward them through dm_helpers_*, which end up calling
* i2c_transfer() / drm_dp_dpcd_*(). These fakes capture the resulting
* messages so the tests can assert what the wrappers built, without touching
* real hardware. KUnit runs cases sequentially, so file-scope capture state
* is reset at the start of each test.
*/
#define FAKE_DDC_MAX_MSGS 4
static struct fake_i2c_capture {
int num;
struct i2c_msg msgs[FAKE_DDC_MAX_MSGS];
} fake_i2c_cap;
static int fake_i2c_master_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num)
{
int i;
fake_i2c_cap.num = num;
for (i = 0; i < num && i < FAKE_DDC_MAX_MSGS; i++)
fake_i2c_cap.msgs[i] = msgs[i];
return num;
}
static u32 fake_i2c_functionality(struct i2c_adapter *adap)
{
return I2C_FUNC_I2C;
}
static const struct i2c_algorithm fake_i2c_algo = {
.master_xfer = fake_i2c_master_xfer,
.functionality = fake_i2c_functionality,
};
static void fake_i2c_lock_bus(struct i2c_adapter *adap, unsigned int flags) {}
static int fake_i2c_trylock_bus(struct i2c_adapter *adap, unsigned int flags)
{
return 1;
}
static void fake_i2c_unlock_bus(struct i2c_adapter *adap, unsigned int flags) {}
static const struct i2c_lock_operations fake_i2c_lock_ops = {
.lock_bus = fake_i2c_lock_bus,
.trylock_bus = fake_i2c_trylock_bus,
.unlock_bus = fake_i2c_unlock_bus,
};
static struct fake_aux_capture {
int calls;
u8 request;
unsigned int address;
size_t size;
} fake_aux_cap;
static ssize_t fake_aux_transfer(struct drm_dp_aux *aux,
struct drm_dp_aux_msg *msg)
{
fake_aux_cap.calls++;
fake_aux_cap.request = msg->request;
fake_aux_cap.address = msg->address;
fake_aux_cap.size = msg->size;
msg->reply = DP_AUX_NATIVE_REPLY_ACK;
return msg->size;
}
/**
* alloc_test_ddc_link - connector/link wired to the recording i2c + aux fakes
* @test: KUnit test context for managed allocation
*
* Builds an amdgpu_dm_connector with a fake i2c adapter and a fake DP aux, and
* points link->priv at the connector so dm_helpers_* find it. Returns the
* dc_link that the lp_* wrappers take as their opaque handle.
*/
static struct dc_link *alloc_test_ddc_link(struct kunit *test)
{
struct amdgpu_dm_connector *aconnector = alloc_test_connector(test, 0);
struct amdgpu_i2c_adapter *i2c;
struct dc_link *link;
KUNIT_ASSERT_NOT_NULL(test, aconnector);
i2c = kunit_kzalloc(test, sizeof(*i2c), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, i2c);
i2c->base.algo = &fake_i2c_algo;
i2c->base.lock_ops = &fake_i2c_lock_ops;
aconnector->i2c = i2c;
mutex_init(&aconnector->dm_dp_aux.aux.hw_mutex);
aconnector->dm_dp_aux.aux.transfer = fake_aux_transfer;
/* Skip the DPCD "throw away" probe read so we capture only our access. */
aconnector->dm_dp_aux.aux.dpcd_probe_disabled = true;
link = aconnector->dc_link;
link->priv = aconnector;
return link;
}
/**
* dm_test_lp_write_i2c_builds_single_write_payload - write builds one i2c msg
* @test: KUnit test context
*
* lp_write_i2c() must forward a single write payload carrying the address,
* length and data buffer unchanged.
*/
static void dm_test_lp_write_i2c_builds_single_write_payload(struct kunit *test)
{
struct dc_link *link = alloc_test_ddc_link(test);
u8 data[3] = {0x11, 0x22, 0x33};
bool ok;
memset(&fake_i2c_cap, 0, sizeof(fake_i2c_cap));
ok = lp_write_i2c(link, 0x3a, data, sizeof(data));
KUNIT_EXPECT_TRUE(test, ok);
KUNIT_ASSERT_EQ(test, fake_i2c_cap.num, 1);
/* write => flags without I2C_M_RD */
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[0].flags, 0);
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[0].addr, 0x3a);
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[0].len, (int)sizeof(data));
KUNIT_EXPECT_PTR_EQ(test, fake_i2c_cap.msgs[0].buf, (void *)data);
}
/**
* dm_test_lp_read_i2c_builds_offset_then_read - read builds offset + read msgs
* @test: KUnit test context
*
* lp_read_i2c() must build a 1-byte write of the offset followed by a
* size-byte read into the caller buffer, both at the same address.
*/
static void dm_test_lp_read_i2c_builds_offset_then_read(struct kunit *test)
{
struct dc_link *link = alloc_test_ddc_link(test);
u8 data[4];
bool ok;
memset(&fake_i2c_cap, 0, sizeof(fake_i2c_cap));
ok = lp_read_i2c(link, 0x50, 0x07, data, sizeof(data));
KUNIT_EXPECT_TRUE(test, ok);
KUNIT_ASSERT_EQ(test, fake_i2c_cap.num, 2);
/* first: 1-byte write of the offset */
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[0].flags, 0);
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[0].addr, 0x50);
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[0].len, 1);
/* second: size-byte read into the caller buffer */
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[1].flags, I2C_M_RD);
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[1].addr, 0x50);
KUNIT_EXPECT_EQ(test, (int)fake_i2c_cap.msgs[1].len, (int)sizeof(data));
KUNIT_EXPECT_PTR_EQ(test, fake_i2c_cap.msgs[1].buf, (void *)data);
}
/**
* dm_test_lp_write_dpcd_forwards_native_write - write forwards a native write
* @test: KUnit test context
*
* lp_write_dpcd() must issue a single DP_AUX_NATIVE_WRITE at the requested
* address for the requested size.
*/
static void dm_test_lp_write_dpcd_forwards_native_write(struct kunit *test)
{
struct dc_link *link = alloc_test_ddc_link(test);
u8 data[2] = {0xDE, 0xAD};
bool ok;
memset(&fake_aux_cap, 0, sizeof(fake_aux_cap));
ok = lp_write_dpcd(link, 0x68000, data, sizeof(data));
KUNIT_EXPECT_TRUE(test, ok);
KUNIT_EXPECT_EQ(test, fake_aux_cap.calls, 1);
KUNIT_EXPECT_EQ(test, (int)fake_aux_cap.request, DP_AUX_NATIVE_WRITE);
KUNIT_EXPECT_EQ(test, fake_aux_cap.address, 0x68000u);
KUNIT_EXPECT_EQ(test, (int)fake_aux_cap.size, (int)sizeof(data));
}
/**
* dm_test_lp_read_dpcd_forwards_native_read - read forwards a native read
* @test: KUnit test context
*
* lp_read_dpcd() must issue a single DP_AUX_NATIVE_READ at the requested
* address for the requested size.
*/
static void dm_test_lp_read_dpcd_forwards_native_read(struct kunit *test)
{
struct dc_link *link = alloc_test_ddc_link(test);
u8 data[4];
bool ok;
memset(&fake_aux_cap, 0, sizeof(fake_aux_cap));
ok = lp_read_dpcd(link, 0x00220, data, sizeof(data));
KUNIT_EXPECT_TRUE(test, ok);
KUNIT_EXPECT_EQ(test, fake_aux_cap.calls, 1);
KUNIT_EXPECT_EQ(test, (int)fake_aux_cap.request, DP_AUX_NATIVE_READ);
KUNIT_EXPECT_EQ(test, fake_aux_cap.address, 0x00220u);
KUNIT_EXPECT_EQ(test, (int)fake_aux_cap.size, (int)sizeof(data));
}
/**
* dm_test_lp_write_i2c_no_connector_returns_false - missing connector fails
* @test: KUnit test context
*
* When link->priv has no connector, dm_helpers_submit_i2c() cannot proceed,
* so lp_write_i2c() must report failure without invoking the adapter.
*/
static void dm_test_lp_write_i2c_no_connector_returns_false(struct kunit *test)
{
struct dc_link *link = alloc_test_ddc_link(test);
u8 data[2] = {0x01, 0x02};
bool ok;
link->priv = NULL;
memset(&fake_i2c_cap, 0, sizeof(fake_i2c_cap));
ok = lp_write_i2c(link, 0x3a, data, sizeof(data));
KUNIT_EXPECT_FALSE(test, ok);
KUNIT_EXPECT_EQ(test, fake_i2c_cap.num, 0);
}
/**
* dm_test_lp_read_dpcd_no_connector_returns_false - missing connector fails
* @test: KUnit test context
*
* When link->priv has no connector, dm_helpers_dp_read_dpcd() cannot proceed,
* so lp_read_dpcd() must report failure without invoking the aux transfer.
*/
static void dm_test_lp_read_dpcd_no_connector_returns_false(struct kunit *test)
{
struct dc_link *link = alloc_test_ddc_link(test);
u8 data[4];
bool ok;
link->priv = NULL;
memset(&fake_aux_cap, 0, sizeof(fake_aux_cap));
ok = lp_read_dpcd(link, 0x00220, data, sizeof(data));
KUNIT_EXPECT_FALSE(test, ok);
KUNIT_EXPECT_EQ(test, fake_aux_cap.calls, 0);
}
/* End of tests for lp_write_i2c() / lp_read_i2c() / lp_write_dpcd() / lp_read_dpcd() */
/*
* Tests for lp_atomic_write_poll_read_i2c() / lp_atomic_write_poll_read_aux()
*
* These wrappers cast the opaque handle to a dc_link and forward to the
* dc_fused_io helpers. The success path submits a fused-IO command sequence to
* the DMCUB, which is out of reach for a unit test, so the coverage here is the
* hardware-free early returns: a NULL link and a payload that fails conversion
* (op size larger than the fused request buffer).
*/
/**
* dm_test_lp_atomic_i2c_null_handle_returns_false - NULL link fails cleanly
* @test: KUnit test context
*
* With a NULL handle the forwarded dc_link is NULL, so the helper must return
* false without dereferencing anything.
*/
static void dm_test_lp_atomic_i2c_null_handle_returns_false(struct kunit *test)
{
struct mod_hdcp_atomic_op_i2c op = { 0 };
KUNIT_EXPECT_FALSE(test,
lp_atomic_write_poll_read_i2c(NULL, &op, &op, &op, 0, 0));
}
/**
* dm_test_lp_atomic_i2c_oversized_op_returns_false - bad payload fails conversion
* @test: KUnit test context
*
* An op whose size exceeds the fused request buffer must fail conversion, so
* the helper returns false before any fused-IO submission. no_ddc_pin routes
* the DDC line through aux_hw_inst, avoiding the GPIO pin dereference.
*/
static void dm_test_lp_atomic_i2c_oversized_op_returns_false(struct kunit *test)
{
struct amdgpu_dm_connector *aconnector = alloc_test_connector(test, 0);
struct mod_hdcp_atomic_op_i2c write = { .size = 0x100 };
struct mod_hdcp_atomic_op_i2c op = { 0 };
struct dc_link *link;
KUNIT_ASSERT_NOT_NULL(test, aconnector);
link = aconnector->dc_link;
link->no_ddc_pin = true;
KUNIT_EXPECT_FALSE(test,
lp_atomic_write_poll_read_i2c(link, &write, &op, &op, 0, 0));
}
/**
* dm_test_lp_atomic_aux_null_handle_returns_false - NULL link fails cleanly
* @test: KUnit test context
*
* With a NULL handle the forwarded dc_link is NULL, so the helper must return
* false without dereferencing anything.
*/
static void dm_test_lp_atomic_aux_null_handle_returns_false(struct kunit *test)
{
struct mod_hdcp_atomic_op_aux op = { 0 };
KUNIT_EXPECT_FALSE(test,
lp_atomic_write_poll_read_aux(NULL, &op, &op, &op, 0, 0));
}
/**
* dm_test_lp_atomic_aux_oversized_op_returns_false - bad payload fails conversion
* @test: KUnit test context
*
* The aux helper reads the DDC line from link->ddc->ddc_pin->pin_data before
* converting, so a minimal pin chain is wired up. An op larger than the fused
* request buffer then fails conversion and the helper returns false without
* any fused-IO submission.
*/
static void dm_test_lp_atomic_aux_oversized_op_returns_false(struct kunit *test)
{
struct amdgpu_dm_connector *aconnector = alloc_test_connector(test, 0);
struct mod_hdcp_atomic_op_aux write = { .size = 0x100 };
struct mod_hdcp_atomic_op_aux op = { 0 };
struct ddc_service *ddc;
struct ddc *ddc_pin;
struct dc_link *link;
void *pin_data;
KUNIT_ASSERT_NOT_NULL(test, aconnector);
ddc = kunit_kzalloc(test, sizeof(*ddc), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, ddc);
ddc_pin = kunit_kzalloc(test, sizeof(*ddc_pin), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, ddc_pin);
/* Only ->en is read; over-allocate so struct gpio stays opaque here. */
pin_data = kunit_kzalloc(test, 128, GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, pin_data);
ddc_pin->pin_data = pin_data;
ddc->ddc_pin = ddc_pin;
link = aconnector->dc_link;
link->ddc = ddc;
KUNIT_EXPECT_FALSE(test,
lp_atomic_write_poll_read_aux(link, &write, &op, &op, 0, 0));
}
/* End of tests for lp_atomic_write_poll_read_i2c() / lp_atomic_write_poll_read_aux() */
/*
* Tests for hdcp_update_display() / hdcp_remove_display() /
* hdcp_reset_display().
@ -1994,6 +2360,18 @@ static struct kunit_case dm_hdcp_test_cases[] = {
KUNIT_CASE(dm_test_srm_data_write_uninitialized_ta_keeps_srm),
KUNIT_CASE(dm_test_srm_data_read_uninitialized_ta_returns_einval),
KUNIT_CASE(dm_test_srm_data_read_empty_srm_returns_zero),
/* lp_write_i2c() / lp_read_i2c() / lp_write_dpcd() / lp_read_dpcd() */
KUNIT_CASE(dm_test_lp_write_i2c_builds_single_write_payload),
KUNIT_CASE(dm_test_lp_read_i2c_builds_offset_then_read),
KUNIT_CASE(dm_test_lp_write_dpcd_forwards_native_write),
KUNIT_CASE(dm_test_lp_read_dpcd_forwards_native_read),
KUNIT_CASE(dm_test_lp_write_i2c_no_connector_returns_false),
KUNIT_CASE(dm_test_lp_read_dpcd_no_connector_returns_false),
/* lp_atomic_write_poll_read_i2c() / lp_atomic_write_poll_read_aux() */
KUNIT_CASE(dm_test_lp_atomic_i2c_null_handle_returns_false),
KUNIT_CASE(dm_test_lp_atomic_i2c_oversized_op_returns_false),
KUNIT_CASE(dm_test_lp_atomic_aux_null_handle_returns_false),
KUNIT_CASE(dm_test_lp_atomic_aux_oversized_op_returns_false),
/* hdcp_update_display() / hdcp_remove_display() / hdcp_reset_display() */
KUNIT_CASE(dm_test_hdcp_update_display_enable_registers_connector),
KUNIT_CASE(dm_test_hdcp_update_display_disable_sets_status_off),