blob: eebbdc69ea0ce449b64a7fa4f7cbd25c6e462106 [file] [log] [blame]
// Copyright 2022 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-i915/ddi-aux-channel.h"
#include <lib/ddk/debug.h>
#include <lib/mmio/mmio.h>
#include <lib/zx/result.h>
#include <lib/zx/time.h>
#include <zircon/assert.h>
#include <zircon/types.h>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <hwreg/bitfields.h>
#include "src/graphics/display/drivers/intel-i915/pci-ids.h"
#include "src/graphics/display/drivers/intel-i915/poll-until.h"
#include "src/graphics/display/drivers/intel-i915/registers-ddi.h"
namespace i915 {
DdiAuxChannel::DdiAuxChannel(fdf::MmioBuffer* mmio_buffer, DdiId ddi_id, uint16_t device_id)
: mmio_buffer_(mmio_buffer), large_timeout_us_(0) {
ZX_ASSERT(mmio_buffer);
if (is_skl(device_id) || is_kbl(device_id)) {
aux_control_ = registers::DdiAuxControl::GetForKabyLakeDdi(ddi_id).ReadFrom(mmio_buffer);
large_timeout_us_ = 1'600;
} else if (is_tgl(device_id)) {
aux_control_ = registers::DdiAuxControl::GetForTigerLakeDdi(ddi_id).ReadFrom(mmio_buffer);
large_timeout_us_ = 4'000;
} else if (is_test_device(device_id)) {
// Stubbed for integration tests.
} else {
ZX_ASSERT_MSG(false, "Unsupported device ID %x", device_id);
}
#if ZX_DEBUG_ASSERT_IMPLEMENTED
ddi_id_ = ddi_id;
device_id_ = device_id;
#endif // ZX_DEBUG_ASSERT_IMPLEMENTED
if (aux_control_.transaction_in_progress()) {
// The boot firmware kicked off an AUX transaction and handed off control to
// the OS without waiting for the transaction to complete.
zxlogf(WARNING, "DDI %u AUX channel initialization blocked by pre-existing transaction.",
ddi_id);
// It's tempting to consider adjusting the AUX parameters below waiting for
// the transaction to complete. However, we're not allowed to write the AUX
// control register while the `transaction_in_progress` bit is set.
if (!WaitForTransactionComplete()) {
// All future transactions will most likely fail. Soldier on and hope the
// DDI miraculously fixes itself.
zxlogf(ERROR,
"DDI %u AUX channel initialization wait for pre-existing transaction timed out.",
ddi_id);
}
}
}
zx::result<DdiAuxChannel::ReplyInfo> DdiAuxChannel::DoTransaction(
const Request& request, cpp20::span<uint8_t> reply_data_buffer) {
WriteRequestForTesting(request);
zx::result<> transact_status = TransactForTesting();
if (!transact_status.is_ok()) {
return transact_status.take_error();
}
return zx::ok(ReadReplyForTesting(reply_data_buffer));
}
void DdiAuxChannel::SetUseThunderbolt(bool use_thunderbolt) {
#if ZX_DEBUG_ASSERT_IMPLEMENTED
if (use_thunderbolt) {
ZX_DEBUG_ASSERT(is_tgl(device_id_));
ZX_DEBUG_ASSERT(ddi_id_ >= DdiId::DDI_TC_1);
ZX_DEBUG_ASSERT(ddi_id_ <= DdiId::DDI_TC_6);
}
#endif // ZX_DEBUG_ASSERT_IMPLEMENTED
aux_control_.set_use_thunderbolt(use_thunderbolt);
}
DdiAuxChannelConfig DdiAuxChannel::Config() const {
int16_t timeout_us;
switch (aux_control_.timeout_timer_select()) {
case registers::DdiAuxControl::kTimeoutUnsupported400us:
timeout_us = 400;
break;
case registers::DdiAuxControl::kTimeout600us:
timeout_us = 600;
break;
case registers::DdiAuxControl::kTimeout800us:
timeout_us = 800;
break;
case registers::DdiAuxControl::kTimeoutLarge:
timeout_us = large_timeout_us_;
break;
}
// The casts are lossless because the pulse counts are 5-bit fields.
const int8_t raw_fast_wake_sync_pulse_count =
static_cast<int8_t>(aux_control_.fast_wake_sync_pulse_count());
const int8_t raw_sync_pulse_count = static_cast<int8_t>(aux_control_.sync_pulse_count());
// The additions and casts do not overflow (which would be UB) because raw
// pulse counts are between 0 and 31.
const int8_t fast_wake_sync_pulse_count = static_cast<int8_t>(raw_fast_wake_sync_pulse_count + 1);
const int8_t sync_pulse_count = static_cast<int8_t>(raw_sync_pulse_count + 1);
return {
.timeout_us = timeout_us,
.sync_pulse_count = sync_pulse_count,
.fast_wake_sync_pulse_count = fast_wake_sync_pulse_count,
// The cast is lossless because use_thunderbolt() is a 1-bit field.
.use_thunderbolt = static_cast<bool>(aux_control_.use_thunderbolt()),
};
}
void DdiAuxChannel::Log() {
const DdiAuxChannelConfig config = Config();
zxlogf(TRACE, "Timeout: %d us", config.timeout_us);
zxlogf(TRACE, "SYNC pulses: %d standard, %d fast wake", config.sync_pulse_count,
config.fast_wake_sync_pulse_count);
zxlogf(TRACE, "Use Thunderbolt: %s", config.use_thunderbolt ? "yes" : "no");
zxlogf(TRACE, "DDI_AUX_CTL: %x", aux_control_.reg_value());
}
void DdiAuxChannel::WriteRequestForTesting(const Request& request) {
WriteRequestHeader(request.command, request.address, request.op_size);
WriteRequestData(request.data);
// WriteRequestData asserts that request.data.size() is <= kMaxOpSize.
static_assert(kMaxOpSize + 4 <= std::numeric_limits<uint32_t>::max());
// Transact() will call WriteTo() after setting more fields.
aux_control_.set_message_size(static_cast<uint32_t>(4 + request.data.size()));
}
bool DdiAuxChannel::WaitForTransactionComplete() {
return PollUntil(
[&] {
aux_control_.ReadFrom(mmio_buffer_);
// Wait for transaction_in_progress() to be cleared, so we know we're
// allowed to write to the AUX control register. Also wait for
// transaction_done() to be set, so we know we'll get meaningful results
// when we read the AUX data registers.
return !aux_control_.transaction_in_progress() && aux_control_.transaction_done();
},
zx::usec(1), kDdiTransactionTimeoutUs);
}
void DdiAuxChannel::WriteRequestHeader(int8_t command, int32_t address, int8_t op_size) {
ZX_ASSERT(command >= 0);
ZX_ASSERT(command <= kMaxCommand);
ZX_ASSERT(address >= 0);
ZX_ASSERT(address <= kMaxAddress);
// For now, we don't handle zero-byte operations. (However, they can be used
// for checking whether there is an I2C device at a given address.)
ZX_ASSERT(op_size > 0);
ZX_ASSERT(op_size <= kMaxOpSize);
const uint8_t byte0 = static_cast<uint8_t>((command << 4) | (address >> 16));
const uint8_t byte1 = static_cast<uint8_t>(address >> 8);
const uint8_t byte2 = static_cast<uint8_t>(address);
const uint8_t byte3 = static_cast<uint8_t>(op_size - 1);
// The most significant byte in each 32-bit register gets transmitted first.
// Intel machines are little-endian, so the transmission order doesn't match
// the memory order.
//
// The compiler will optimize away redundant shifts.
const uint32_t swapped_bytes =
(uint32_t{byte0} << 24) | (uint32_t{byte1} << 16) | (uint32_t{byte2} << 8) | uint32_t{byte3};
auto aux_data_header = registers::DdiAuxData::GetData0ForAuxControl(aux_control_).FromValue(0);
aux_data_header.set_swapped_bytes(swapped_bytes).WriteTo(mmio_buffer_);
}
void DdiAuxChannel::WriteRequestData(cpp20::span<const uint8_t> data) {
ZX_ASSERT(data.size() <= kMaxOpSize);
// Points to the data byte currently copied into the AUX DDI buffer.
const uint8_t* data_pointer = data.data();
// Points 4 bytes below the MMIO address of the AUX DDI buffer being written to.
zx_off_t aux_data_mmio_address =
registers::DdiAuxData::GetData0ForAuxControl(aux_control_).addr();
// The cast is lossless because data.size() is at most 16.
int data_left = static_cast<int>(data.size());
while (data_left > 0) {
uint32_t swapped_bytes;
if (data_left >= 4) {
// Fast path. This gets optimized to one `bswap` instruction.
swapped_bytes = (uint32_t{data_pointer[0]} << 24) | (uint32_t{data_pointer[1]} << 16) |
(uint32_t{data_pointer[2]} << 8) | uint32_t{data_pointer[3]};
data_left -= 4;
data_pointer += 4;
} else {
uint8_t raw_bytes[] = {0, 0, 0, 0};
std::memcpy(&raw_bytes[0], data_pointer, data_left);
swapped_bytes = (uint32_t{raw_bytes[0]} << 24) | (uint32_t{raw_bytes[1]} << 16) |
(uint32_t{raw_bytes[2]} << 8) | uint32_t{raw_bytes[3]};
data_left = 0;
// We don't need to update `data_pointer` on the slow path because we'll
// exit the loop. Adding 4 here would yield undefined behavior, because it
// would get the pointer past the buffer it points to.
}
aux_data_mmio_address += 4;
mmio_buffer_->Write32(swapped_bytes, aux_data_mmio_address);
}
}
zx::result<> DdiAuxChannel::TransactForTesting() {
// If the AUX control register works as documented, it should be sufficient to
// call FixConfig() once, to adjust the configuration left over from the boot
// firmware.
//
// Calling FixConfig() every transaction ensures the configuration is still
// what we expect even if the control register's configuration fields changed
// while we were reading it in a previous execution of Transact().
FixConfig();
// Resets the R/WC (Read/Write-Clear) indicators. This guarantees the
// indicators are meaningful when the transaction completes.
aux_control_.set_transaction_done(true).set_timeout(true).set_receive_error(true);
// Setting this field kicks off the transaction. The write also picks up the
// `message_size` field change done in WriteRequest().
aux_control_.set_transaction_in_progress(true).WriteTo(mmio_buffer_);
if (!WaitForTransactionComplete()) {
// The DDI did not complete the transaction (which includes reporting an AUX
// timeout) in the allotted time. This is most likely a hardware error.
zxlogf(WARNING, "DDI did not complete / fail AUX transaction in %d us",
kDdiTransactionTimeoutUs);
return zx::make_result(ZX_ERR_IO_MISSED_DEADLINE);
}
if (aux_control_.timeout()) {
// AUX timeouts are expected for slow devices, so this condition does not
// warrant serious logging.
//
// For example, the maximum AUX timeout supported by Kaby Lake and Skylake
// is 1,600us but, since DisplayPort 1.4a, sinks are allowed 3,200us (3.2ms)
// to reply to AUX transactions right after the hot-plug detect event, and
// when woken up from a low power state.
//
// The 3.2ms timeout comes from the DisplayPort 2.0 standard version 2.0,
// section 2.11.2 "AUX Trransaction Response/Reply Timeouts", page 382.
zxlogf(TRACE, "DDI reported AUX transaction timeout. This is normal after HPD or wakeup.");
return zx::make_result(ZX_ERR_IO_MISSED_DEADLINE);
}
if (aux_control_.receive_error()) {
zxlogf(WARNING, "DDI AUX receive error. Data corrupted or incorrect bit count.");
return zx::make_result(ZX_ERR_IO_DATA_INTEGRITY);
}
// The cast is lossless because message_size() is a 5-bit field.
const int reply_size = static_cast<int>(aux_control_.message_size());
// AUX replies must contain at least one command byte. AUX replies can contain
// at most 16 data bytes, asides from the header byte.
if (reply_size == 0 || reply_size > 1 + kMaxOpSize) {
zxlogf(WARNING, "DDI AUX invalid reply size: %d bytes", reply_size);
return zx::make_result(ZX_ERR_IO_DATA_INTEGRITY);
}
return zx::ok();
}
// Reads an AUX channel response from the DDI's data buffer.
DdiAuxChannel::ReplyInfo DdiAuxChannel::ReadReplyForTesting(cpp20::span<uint8_t> data_buffer) {
// We rely on the fact that Transact() must have done an MMIO read for
// `aux_control_` before exiting successfully.
//
// Transact() would not have returned success if any of these predicates is
// false.
ZX_ASSERT(!aux_control_.transaction_in_progress());
ZX_ASSERT(!aux_control_.receive_error());
ZX_ASSERT(!aux_control_.timeout());
ZX_ASSERT(aux_control_.transaction_done());
ZX_ASSERT(aux_control_.message_size() >= 1);
// The cast is lossless because message_size() is a 5-bit field.
const int8_t aux_message_size = static_cast<int8_t>(aux_control_.message_size());
// The cast is lossless because `aux_message_size` is between 0 and 31. Also,
// we checked above that `aux_message_size` is at least 1.
const int8_t aux_data_size = static_cast<int8_t>(aux_message_size - 1);
// The cast is lossless because the min() result is at most `aux_data_size`,
// which is is at most 31.
int data_left = static_cast<int>(std::min<size_t>(aux_data_size, data_buffer.size()));
// The first AUX data register is a special case, because it contains the
// headear byte.
auto aux_data_start =
registers::DdiAuxData::GetData0ForAuxControl(aux_control_).ReadFrom(mmio_buffer_);
// Points at the next byte to be written in the data buffer.
uint8_t* data_pointer = data_buffer.data();
// This gets optimized to one `bswap` instruction.
const uint8_t data_start_bytes[4] = {static_cast<uint8_t>(aux_data_start.swapped_bytes() >> 24),
static_cast<uint8_t>(aux_data_start.swapped_bytes() >> 16),
static_cast<uint8_t>(aux_data_start.swapped_bytes() >> 8),
static_cast<uint8_t>(aux_data_start.swapped_bytes())};
// Save the command byte so we can return it later. The compiler will optimize
// away the extra variable.
const uint8_t header_byte = data_start_bytes[0];
{
const int copy_size = std::min(data_left, 3);
std::memcpy(data_pointer, data_start_bytes + 1, copy_size);
data_pointer += copy_size;
data_left -= copy_size;
}
// Points 4 bytes below the MMIO address of the AUX DDI buffer being read.
zx_off_t aux_data_mmio_address = aux_data_start.reg_addr();
while (data_left > 0) {
aux_data_mmio_address += 4;
const uint32_t swapped_bytes = mmio_buffer_->Read32(aux_data_mmio_address);
if (data_left >= 4) {
// Fast path. This gets optimized to one `bswap` instruction.
data_pointer[0] = static_cast<uint8_t>(swapped_bytes >> 24);
data_pointer[1] = static_cast<uint8_t>(swapped_bytes >> 16);
data_pointer[2] = static_cast<uint8_t>(swapped_bytes >> 8);
data_pointer[3] = static_cast<uint8_t>(swapped_bytes);
data_left -= 4;
data_pointer += 4;
} else {
// This gets optimized to one `bswap` instruction.
const uint8_t data_bytes[4] = {
static_cast<uint8_t>(swapped_bytes >> 24), static_cast<uint8_t>(swapped_bytes >> 16),
static_cast<uint8_t>(swapped_bytes >> 8), static_cast<uint8_t>(swapped_bytes)};
std::memcpy(data_pointer, data_bytes, data_left);
// We don't need to update `data_pointer` on the slow path because we'll
// exit the loop. Adding 4 here would yield undefined behavior, because it
// would get the pointer past the buffer it points to.
data_left = 0;
}
}
return {.reply_header = header_byte, .reply_data_size = aux_data_size};
}
void DdiAuxChannel::FixConfig() {
// TODO(https://fxbug.dev/42106274): Support interrupts
aux_control_.set_interrupt_on_done(true);
if (aux_control_.timeout_timer_select() != registers::DdiAuxControl::kTimeoutLarge) {
zxlogf(TRACE, "DDI AUX channel transaction timeout select was %u. Set to maximum.",
aux_control_.timeout_timer_select());
aux_control_.set_timeout_timer_select(registers::DdiAuxControl::kTimeoutLarge);
}
if (aux_control_.fast_wake_sync_pulse_count() !=
registers::DdiAuxControl::kFastWakeSyncPulseCount) {
zxlogf(WARNING, "DDI AUX channel fast wake pulse count was incorrectly set to %u. Fixed.",
aux_control_.fast_wake_sync_pulse_count());
aux_control_.set_fast_wake_sync_pulse_count(registers::DdiAuxControl::kFastWakeSyncPulseCount);
}
if (aux_control_.sync_pulse_count() < registers::DdiAuxControl::kMinSyncPulseCount) {
zxlogf(WARNING, "DDI AUX channel wake pulse count was incorrectly set to %u. Fixed.",
aux_control_.sync_pulse_count());
aux_control_.set_sync_pulse_count(registers::DdiAuxControl::kMinSyncPulseCount);
}
}
} // namespace i915