blob: 3e84a69ee6d923f33fca3e86a6772d7903a6341e [file] [log] [blame]
// Copyright 2020 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 "aml-ram.h"
#include <lib/device-protocol/pdev.h>
#include <lib/zx/clock.h>
#include <zircon/assert.h>
#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/driver.h>
#include <ddk/platform-defs.h>
#include <ddktl/fidl.h>
#include <fbl/alloc_checker.h>
#include <fbl/auto_lock.h>
#include <soc/aml-s905d2/s905d2-hw.h>
namespace amlogic_ram {
constexpr zx_signals_t kCancelSignal = ZX_USER_SIGNAL_0;
constexpr zx_signals_t kWorkPendingSignal = ZX_USER_SIGNAL_1;
constexpr size_t kMaxPendingRequests = 64u;
zx_status_t ValidateRequest(const ram_metrics::BandwidthMeasurementConfig& config) {
// Restrict timer to reasonable values.
if ((config.cycles_to_measure < kMinimumCycleCount) ||
(config.cycles_to_measure > kMaximumCycleCount)) {
return ZX_ERR_INVALID_ARGS;
}
for (size_t ix = 0; ix != ram_metrics::MAX_COUNT_CHANNELS; ++ix) {
auto& channel = config.channels[ix];
if ((ix >= MEMBW_MAX_CHANNELS) && (channel != 0)) {
// We only support the first four channels.
return ZX_ERR_INVALID_ARGS;
}
if (channel > 0xffffffff) {
// We don't support sub-ports (bits above 31) yet.
return ZX_ERR_NOT_SUPPORTED;
}
}
return ZX_OK;
}
zx_status_t AmlRam::Create(void* context, zx_device_t* parent) {
zx_status_t status;
ddk::PDev pdev(parent);
std::optional<ddk::MmioBuffer> mmio;
if ((status = pdev.MapMmio(0, &mmio)) != ZX_OK) {
zxlogf(ERROR, "aml-ram: Failed to map mmio, st = %d", status);
return status;
}
fbl::AllocChecker ac;
auto device = fbl::make_unique_checked<AmlRam>(&ac, parent, *std::move(mmio));
if (!ac.check()) {
zxlogf(ERROR, "aml-ram: Failed to allocate device memory");
return ZX_ERR_NO_MEMORY;
}
status = device->DdkAdd("ram", DEVICE_ADD_NON_BINDABLE);
if (status != ZX_OK) {
zxlogf(ERROR, "aml-ram: Failed to add ram device, st = %d", status);
return status;
}
__UNUSED auto* dummy = device.release();
return ZX_OK;
}
AmlRam::AmlRam(zx_device_t* parent, ddk::MmioBuffer mmio)
: DeviceType(parent), mmio_(std::move(mmio)) {}
AmlRam::~AmlRam() {
// Verify we drained all requests.
ZX_ASSERT(requests_.empty());
}
void AmlRam::DdkSuspend(ddk::SuspendTxn txn) {
// TODO(cpu): First put the device into txn.requested_state().
if (txn.suspend_reason() & (DEVICE_SUSPEND_REASON_POWEROFF | DEVICE_SUSPEND_REASON_MEXEC |
DEVICE_SUSPEND_REASON_REBOOT)) {
// Do any additional cleanup that is needed while shutting down the driver.
Shutdown();
}
txn.Reply(ZX_OK, txn.requested_state());
}
void AmlRam::DdkRelease() {
Shutdown();
delete this;
}
zx_status_t AmlRam::DdkMessage(fidl_msg_t* msg, fidl_txn_t* txn) {
DdkTransaction transaction(txn);
ram_metrics::Device::Dispatch(this, msg, &transaction);
return transaction.Status();
}
void AmlRam::MeasureBandwidth(ram_metrics::BandwidthMeasurementConfig config,
MeasureBandwidthCompleter::Sync completer) {
zx_status_t st = ValidateRequest(config);
if (st != ZX_OK) {
zxlogf(ERROR, "aml-ram: bad request\n");
completer.ReplyError(st);
return;
}
if (!thread_control_) {
// First request.
auto status = zx::event::create(0u, &thread_control_);
if (status != ZX_OK) {
completer.ReplyError(st);
zxlogf(ERROR, "aml-ram: could not create event\n");
return;
}
thread_ = std::thread([this] { ReadLoop(); });
}
{
fbl::AutoLock lock(&lock_);
if (requests_.size() > kMaxPendingRequests) {
// Once the queue is shorter the request would likely succeed.
completer.ReplyError(ZX_ERR_SHOULD_WAIT);
return;
}
// Enqueue task and signal worker thread as needed.
requests_.emplace_back(std::move(config), completer.ToAsync());
if (requests_.size() == 1u) {
thread_control_.signal(0, kWorkPendingSignal);
}
}
}
zx_status_t AmlRam::ReadBandwithCounters(const ram_metrics::BandwidthMeasurementConfig& config,
ram_metrics::BandwidthInfo* bpi) {
uint32_t channels_enabled = 0u;
for (size_t ix = 0; ix != MEMBW_MAX_CHANNELS; ++ix) {
channels_enabled |= (config.channels[ix] != 0) ? (1u << ix) : 0;
mmio_.Write32(static_cast<uint32_t>(config.channels[ix]), MEMBW_RP[ix]);
mmio_.Write32(0xffff, MEMBW_SP[ix]);
}
if (channels_enabled == 0x0) {
// Nothing to monitor.
return ZX_OK;
}
mmio_.Write32(static_cast<uint32_t>(config.cycles_to_measure), MEMBW_TIMER);
mmio_.Write32(channels_enabled | DMC_QOS_ENABLE_CTRL, MEMBW_PORTS_CTRL);
bpi->timestamp = zx_clock_get_monotonic();
uint32_t value = mmio_.Read32(MEMBW_PORTS_CTRL);
uint32_t count = 0;
// Polling loop for the bandwith cycle counters.
// TODO(cpu): tune wait interval to minimize polling.
while (value & DMC_QOS_ENABLE_CTRL) {
if (count++ > 20) {
return ZX_ERR_TIMED_OUT;
}
auto status =
thread_control_.wait_one(kCancelSignal, zx::deadline_after(zx::msec(50)), nullptr);
if (status != ZX_ERR_TIMED_OUT) {
// Shutdown. This is handled by the caller.
return ZX_ERR_CANCELED;
}
value = mmio_.Read32(MEMBW_PORTS_CTRL);
}
bpi->channels[0].readwrite_cycles = mmio_.Read32(MEMBW_C0_GRANT_CNT) * 16ul;
bpi->channels[1].readwrite_cycles = mmio_.Read32(MEMBW_C1_GRANT_CNT) * 16ul;
bpi->channels[2].readwrite_cycles = mmio_.Read32(MEMBW_C2_GRANT_CNT) * 16ul;
bpi->channels[3].readwrite_cycles = mmio_.Read32(MEMBW_C3_GRANT_CNT) * 16ul;
mmio_.Write32(channels_enabled | DMC_QOS_CLEAR_CTRL, MEMBW_PORTS_CTRL);
return ZX_OK;
}
void AmlRam::ReadLoop() {
std::deque<Job> jobs;
for (;;) {
zx_signals_t observed = 0u;
auto status = thread_control_.wait_one(kCancelSignal | kWorkPendingSignal, zx::time::infinite(),
&observed);
if (status != ZX_OK) {
zxlogf(ERROR, "aml-ram: error in wait_one, st =%d\n", status);
return;
}
ZX_ASSERT(jobs.empty());
if (observed & kCancelSignal) {
// Shutdown with no pending work in the local queue.
return;
}
{
fbl::AutoLock lock(&lock_);
if (requests_.empty()) {
// Done with all work. Clear pending-work signal, go back to wait.
thread_control_.signal(kWorkPendingSignal, 0);
continue;
} else {
// Some work available. Move it all to the local queue.
jobs = std::move(requests_);
}
}
while (!jobs.empty()) {
Job& job = jobs.front();
ram_metrics::BandwidthInfo bpi;
status = ReadBandwithCounters(job.config, &bpi);
if (status == ZX_OK) {
job.completer.ReplySuccess(bpi);
} else if (status == ZX_ERR_CANCELED) {
// Shutdown case with pending work in local queue. Give back the jobs
// to the main thread.
RevertJobs(&jobs);
return;
} else {
// Unexpected error. Better log it.
zxlogf(ERROR, "aml-ram: read error z %d\n", status);
job.completer.ReplyError(status);
}
jobs.pop_front();
}
};
}
// Merge back the request jobs from the local jobs in |source| preserving
// the order of arrival: the last job in |source| is ahead of the
// first job in |request_|.
void AmlRam::RevertJobs(std::deque<AmlRam::Job>* source) {
fbl::AutoLock lock(&lock_);
while (!source->empty()) {
requests_.push_front(std::move(source->back()));
source->pop_back();
}
}
void AmlRam::Shutdown() {
if (thread_control_) {
thread_control_.signal(kWorkPendingSignal, kCancelSignal);
thread_.join();
thread_control_.reset();
// Cancel all pending requests. There are no more threads
// but we still take the lock to keep lock checker happy.
{
fbl::AutoLock lock(&lock_);
for (auto& request : requests_) {
request.completer.Close(ZX_ERR_CANCELED);
}
requests_.clear();
}
}
}
} // namespace amlogic_ram
static constexpr zx_driver_ops_t aml_ram_driver_ops = []() {
zx_driver_ops_t result = {};
result.version = DRIVER_OPS_VERSION;
result.bind = amlogic_ram::AmlRam::Create;
return result;
}();
// clang-format off
ZIRCON_DRIVER_BEGIN(aml_ram, aml_ram_driver_ops, "zircon", "0.1", 5)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PDEV),
BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_AMLOGIC),
BI_ABORT_IF(NE, BIND_PLATFORM_DEV_DID, PDEV_DID_AMLOGIC_RAM_CTL),
// This driver can likely support S905D3 in the future.
BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_PID, PDEV_PID_AMLOGIC_S905D2),
BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_PID, PDEV_PID_AMLOGIC_T931),
ZIRCON_DRIVER_END(aml_ram)
// clang-format on