blob: d527682808ca9299f30b34db35231c9ecbc859fd [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/graphics/display/drivers/intel-display/dp-aux-channel-impl.h"
#include <lib/driver/logging/cpp/logger.h>
#include <lib/driver/mmio/cpp/mmio-buffer.h>
#include <lib/stdcompat/span.h>
#include <lib/zx/result.h>
#include <threads.h>
#include <zircon/assert.h>
#include <zircon/errors.h>
#include <algorithm>
#include <cstdint>
#include <fbl/auto_lock.h>
#include "src/graphics/display/drivers/intel-display/ddi-aux-channel.h"
#include "src/graphics/display/drivers/intel-display/hardware-common.h"
namespace intel_display {
namespace {
// Aux port functions
// 4-bit request type in Aux channel request messages.
enum {
DP_REQUEST_I2C_WRITE = 0,
DP_REQUEST_I2C_READ = 1,
DP_REQUEST_NATIVE_WRITE = 8,
DP_REQUEST_NATIVE_READ = 9,
};
// 4-bit statuses in Aux channel reply messages.
enum {
DP_REPLY_AUX_ACK = 0,
DP_REPLY_AUX_NACK = 1,
DP_REPLY_AUX_DEFER = 2,
DP_REPLY_I2C_NACK = 4,
DP_REPLY_I2C_DEFER = 8,
};
// The I2C address for writing the DDC segment.
//
// VESA Enhanced Display Data Channel (E-DDC) Standard version 1.3 revised
// Dec 31 2020, Section 2.2.3 "DDC Addresses", page 17.
constexpr uint8_t kDdcSegmentI2cTargetAddress = 0x30;
// The I2C address for writing the DDC data offset/reading DDC data.
//
// VESA Enhanced Display Data Channel (E-DDC) Standard version 1.3 revised
// Dec 31 2020, Section 2.2.3 "DDC Addresses", page 17.
constexpr uint8_t kDdcDataI2cTargetAddress = 0x50;
} // namespace
zx::result<DdiAuxChannel::ReplyInfo> DpAuxChannelImpl::DoTransaction(
const DdiAuxChannel::Request& request, cpp20::span<uint8_t> reply_data_buffer) {
// If the DisplayPort sink device isn't ready to handle an Aux message,
// it can return an AUX_DEFER reply, which means we should retry the
// request. The spec added a requirement for >=7 defer retries in v1.3,
// but there are no requirements before that nor is there a max value. 16
// retries is pretty arbitrary and might need to be increased for slower
// displays.
const int kMaxDefers = 16;
// Per table 2-43 in v1.1a, we need to retry >3 times, since some
// DisplayPort sink devices time out on the first DP aux request
// but succeed on later requests.
const int kMaxTimeouts = 5;
unsigned defers_seen = 0;
unsigned timeouts_seen = 0;
for (;;) {
zx::result<DdiAuxChannel::ReplyInfo> transaction_result =
aux_channel_.DoTransaction(request, reply_data_buffer);
if (transaction_result.is_error()) {
if (transaction_result.error_value() == ZX_ERR_IO_MISSED_DEADLINE) {
if (++timeouts_seen == kMaxTimeouts) {
fdf::debug("DP aux: Got too many timeouts ({})", kMaxTimeouts);
return transaction_result;
}
// Retry on timeout.
continue;
}
// We do not retry if sending the raw message failed for
// an unexpected reason.
return transaction_result;
}
uint8_t header_byte = transaction_result->reply_header;
uint8_t padding = header_byte & 0xf;
uint8_t status = static_cast<uint8_t>(header_byte >> 4);
// Sanity check: The padding should be zero. If it's not, we
// shouldn't return an error, in case this space gets used for some
// later extension to the protocol. But report it, in case this
// indicates some problem.
if (padding) {
fdf::info("DP aux: Reply header padding is non-zero (header byte: 0x{:x})", header_byte);
}
switch (status) {
case DP_REPLY_AUX_ACK:
// The AUX_ACK implies that we got an I2C ACK too.
return transaction_result;
case DP_REPLY_AUX_NACK:
fdf::trace("DP aux: Reply was not an ack (got AUX_NACK)");
return zx::error_result(ZX_ERR_IO_REFUSED);
case DP_REPLY_AUX_DEFER:
if (++defers_seen == kMaxDefers) {
fdf::trace("DP aux: Received too many AUX DEFERs ({})", kMaxDefers);
return zx::error_result(ZX_ERR_IO_MISSED_DEADLINE);
}
// Go around the loop again to retry.
continue;
case DP_REPLY_I2C_NACK:
fdf::trace("DP aux: Reply was not an ack (got I2C_NACK)");
return zx::error_result(ZX_ERR_IO_REFUSED);
case DP_REPLY_I2C_DEFER:
// TODO(https://fxbug.dev/42106274): Implement handling of I2C_DEFER.
fdf::trace("DP aux: Received I2C_DEFER (not implemented)");
return zx::error_result(ZX_ERR_NEXT);
default:
fdf::trace("DP aux: Unrecognized reply (header byte: 0x{:x})", header_byte);
return zx::error_result(ZX_ERR_IO_DATA_INTEGRITY);
}
}
}
zx_status_t DpAuxChannelImpl::DpAuxRead(uint32_t dp_cmd, uint32_t addr, uint8_t* buf, size_t size) {
while (size > 0) {
uint32_t chunk_size = static_cast<uint32_t>(std::min<size_t>(size, DdiAuxChannel::kMaxOpSize));
size_t bytes_read = 0;
zx_status_t status = DpAuxReadChunk(dp_cmd, addr, buf, chunk_size, &bytes_read);
if (status != ZX_OK) {
return status;
}
if (bytes_read == 0) {
// We failed to make progress on the last call. To avoid the
// risk of getting an infinite loop from that happening
// continually, we return.
return ZX_ERR_IO;
}
buf += bytes_read;
size -= bytes_read;
}
return ZX_OK;
}
zx_status_t DpAuxChannelImpl::DpAuxReadChunk(uint32_t dp_cmd, uint32_t addr, uint8_t* buf,
uint32_t size_in, size_t* size_out) {
const DdiAuxChannel::Request request = {
.address = static_cast<int32_t>(addr),
.command = static_cast<int8_t>(dp_cmd),
.op_size = static_cast<int8_t>(size_in),
.data = cpp20::span<uint8_t>(),
};
zx::result<DdiAuxChannel::ReplyInfo> result =
DoTransaction(request, cpp20::span<uint8_t>(buf, size_in));
if (result.is_error()) {
return result.error_value();
}
// The cast is not UB because `reply_data_size` is guaranteed to be between
// 1 and 16.
const size_t bytes_read = static_cast<size_t>(result.value().reply_data_size);
if (static_cast<size_t>(bytes_read) > size_in) {
fdf::warn("DP aux read: Reply was larger than requested");
return ZX_ERR_IO;
}
*size_out = bytes_read;
return ZX_OK;
}
zx_status_t DpAuxChannelImpl::DpAuxWrite(uint32_t dp_cmd, uint32_t addr, const uint8_t* buf,
size_t size) {
// Implement this if it's ever needed
ZX_ASSERT_MSG(size <= 16, "message too large");
const DdiAuxChannel::Request request = {
.address = static_cast<int32_t>(addr),
.command = static_cast<int8_t>(dp_cmd),
.op_size = static_cast<int8_t>(size),
.data = cpp20::span<const uint8_t>(buf, size),
};
// In case of a short write, receives the amount of written bytes.
uint8_t reply_data[1];
zx::result<DdiAuxChannel::ReplyInfo> transaction_result = DoTransaction(request, reply_data);
if (transaction_result.is_error()) {
return transaction_result.error_value();
}
// TODO(https://fxbug.dev/42106274): Handle the case where the hardware did a short write,
// for which we could send the remaining bytes.
if (transaction_result->reply_data_size != 0) {
fdf::warn("DP aux write: Unexpected reply size");
return ZX_ERR_IO;
}
return ZX_OK;
}
zx::result<> DpAuxChannelImpl::ReadEdidBlock(int index,
std::span<uint8_t, edid::kBlockSize> edid_block) {
ZX_DEBUG_ASSERT(index >= 0);
ZX_DEBUG_ASSERT(index < edid::kMaxEdidBlockCount);
// Size of an E-DDC segment.
//
// VESA Enhanced Display Data Channel (E-DDC) Standard version 1.3 revised
// Dec 31 2020, Section 2.2.5 "Segment Pointer", page 18.
static constexpr int kEddcSegmentSize = 256;
static_assert(kEddcSegmentSize == edid::kBlockSize * 2);
fbl::AutoLock lock(&lock_);
// `index / 2` is in [0, 127], so casting it to uint8_t won't overflow.
const uint8_t segment_pointer = static_cast<uint8_t>(index / 2);
zx::result<> write_segment_result =
zx::make_result(DpAuxWrite(DP_REQUEST_I2C_WRITE, kDdcSegmentI2cTargetAddress,
/*buf=*/&segment_pointer, /*size=*/1));
if (write_segment_result.status_value() == ZX_ERR_IO_REFUSED && segment_pointer == 0) {
// A display device that doesn't support E-DDC returns an I2C NACK response
// when the host writes to the segment pointer. Thus, we ignore the NACK
// and perform non-segmented DDC read operations if the segment pointer is
// zero.
fdf::info("E-DDC segment pointer is not supported. Will perform DDC read.");
} else if (write_segment_result.is_error()) {
fdf::error("Failed to write E-DDC segment pointer: {}", write_segment_result);
return write_segment_result.take_error();
}
// Segment offset of the first byte in the current block.
//
// `segment_offset` is either 0 or 128, so casting it to uint8_t won't
// overflow.
const uint8_t segment_offset = static_cast<uint8_t>((index % 2) * edid::kBlockSize);
zx::result<> write_segment_offset_result =
zx::make_result(DpAuxWrite(DP_REQUEST_I2C_WRITE, kDdcDataI2cTargetAddress,
/*buf=*/&segment_offset, /*size=*/1));
if (write_segment_offset_result.is_error()) {
fdf::error("Failed to write E-DDC segment offset: {}", write_segment_offset_result);
return write_segment_offset_result.take_error();
}
zx::result<> read_edid_result = zx::make_result(DpAuxRead(
DP_REQUEST_I2C_READ, kDdcDataI2cTargetAddress, edid_block.data(), edid_block.size()));
if (read_edid_result.is_error()) {
fdf::error("Failed to read E-DDC block #{}: {}", index, read_edid_result);
return read_edid_result.take_error();
}
return zx::ok();
}
bool DpAuxChannelImpl::DpcdRead(uint32_t addr, uint8_t* buf, size_t size) {
fbl::AutoLock lock(&lock_);
constexpr uint32_t kReadAttempts = 3;
for (unsigned i = 0; i < kReadAttempts; i++) {
if (DpAuxRead(DP_REQUEST_NATIVE_READ, addr, buf, size) == ZX_OK) {
return true;
}
zx_nanosleep(zx_deadline_after(ZX_MSEC(5)));
}
return false;
}
bool DpAuxChannelImpl::DpcdWrite(uint32_t addr, const uint8_t* buf, size_t size) {
fbl::AutoLock lock(&lock_);
return DpAuxWrite(DP_REQUEST_NATIVE_WRITE, addr, buf, size) == ZX_OK;
}
DpAuxChannelImpl::DpAuxChannelImpl(fdf::MmioBuffer* mmio_buffer, DdiId ddi_id, uint16_t device_id)
: aux_channel_(mmio_buffer, ddi_id, device_id) {
ZX_ASSERT(mtx_init(&lock_, mtx_plain) == thrd_success);
}
} // namespace intel_display