blob: c98f07c3204b641c97d2260641734aef2663596f [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 "dai.h"
#include <lib/ddk/binding_driver.h>
#include <lib/ddk/debug.h>
#include <lib/ddk/metadata.h>
#include <lib/ddk/platform-defs.h>
#include <lib/fpromise/result.h>
#include <lib/zx/clock.h>
#include <math.h>
#include <string.h>
#include <numeric>
#include <optional>
#include <utility>
#include <fbl/algorithm.h>
namespace audio::aml_g12 {
enum {
FRAGMENT_PDEV,
FRAGMENT_COUNT,
};
AmlG12TdmDai::AmlG12TdmDai(zx_device_t* parent, ddk::PDevFidl pdev)
: AmlG12TdmDaiDeviceType(parent),
loop_(&kAsyncLoopConfigNoAttachToCurrentThread),
pdev_(std::move(pdev)) {
ddk_proto_id_ = ZX_PROTOCOL_DAI;
loop_.StartThread("aml-g12-tdm-dai");
}
void AmlG12TdmDai::InitDaiFormats() {
// Only the PCM signed sample format is supported.
dai_format_.sample_format = ::fuchsia::hardware::audio::DaiSampleFormat::PCM_SIGNED;
dai_format_.frame_rate = AmlTdmConfigDevice::GetSupportedFrameRates()[0];
dai_format_.bits_per_sample = metadata_.dai.bits_per_sample;
dai_format_.bits_per_slot = metadata_.dai.bits_per_slot;
dai_format_.number_of_channels = metadata_.dai.number_of_channels;
dai_format_.channels_to_use_bitmask = std::numeric_limits<uint64_t>::max(); // Enable all.
switch (metadata_.dai.type) {
case metadata::DaiType::I2s:
dai_format_.frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::I2S);
break;
case metadata::DaiType::StereoLeftJustified:
dai_format_.frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::STEREO_LEFT);
break;
case metadata::DaiType::Tdm1:
dai_format_.frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::TDM1);
break;
case metadata::DaiType::Tdm2:
dai_format_.frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::TDM2);
break;
case metadata::DaiType::Tdm3:
dai_format_.frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::TDM3);
break;
}
}
void AmlG12TdmDai::Connect(ConnectRequestView request, ConnectCompleter::Sync& completer) {
::fidl::InterfaceRequest<::fuchsia::hardware::audio::Dai> dai;
dai.set_channel(request->dai_protocol.TakeChannel());
dai_binding_.emplace(this, std::move(dai), loop_.dispatcher());
dai_binding_->set_error_handler([this](zx_status_t status) -> void {
zxlogf(INFO, "DAI protocol %s", zx_status_get_string(status));
Stop([]() {});
delay_info_sent_ = false;
});
}
zx_status_t AmlG12TdmDai::DaiConnect(zx::channel channel) {
dai_binding_.emplace(this, std::move(channel), loop_.dispatcher());
return ZX_OK;
}
void AmlG12TdmDai::Reset(ResetCallback callback) {
auto status =
aml_audio_->InitHW(metadata_, dai_format_.channels_to_use_bitmask, dai_format_.frame_rate);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to init tdm hardware %d", status);
}
callback();
}
zx_status_t AmlG12TdmDai::InitPDev() {
size_t actual = 0;
auto status = device_get_fragment_metadata(parent(), "pdev", DEVICE_METADATA_PRIVATE, &metadata_,
sizeof(metadata::AmlConfig), &actual);
if (status != ZX_OK || sizeof(metadata::AmlConfig) != actual) {
zxlogf(ERROR, "device_get_fragment_metadata failed %d", status);
return status;
}
status = AmlTdmConfigDevice::Normalize(metadata_);
if (status != ZX_OK) {
return status;
}
InitDaiFormats();
status = pdev_.GetBti(0, &bti_);
if (status != ZX_OK) {
zxlogf(ERROR, "could not obtain bti %d", status);
return status;
}
std::optional<fdf::MmioBuffer> mmio;
status = pdev_.MapMmio(0, &mmio);
if (status != ZX_OK) {
zxlogf(ERROR, "could not get mmio %d", status);
return status;
}
aml_audio_ = std::make_unique<AmlTdmConfigDevice>(metadata_, *std::move(mmio));
if (aml_audio_ == nullptr) {
zxlogf(ERROR, "failed to create TDM device with config");
return ZX_ERR_NO_MEMORY;
}
Reset([]() {});
return ZX_OK;
}
void AmlG12TdmDai::DdkRelease() {
loop_.Shutdown();
Shutdown();
delete this;
}
void AmlG12TdmDai::Shutdown() {
if (rb_started_) {
Stop([]() {});
}
aml_audio_->Shutdown();
pinned_ring_buffer_.Unpin();
}
void AmlG12TdmDai::GetVmo(uint32_t min_frames, uint32_t clock_recovery_notifications_per_ring,
GetVmoCallback callback) {
if (rb_started_) {
zxlogf(ERROR, "GetVmo failed, ring buffer started");
ringbuffer_binding_->Unbind();
return;
}
frame_size_ = metadata_.ring_buffer.number_of_channels * metadata_.ring_buffer.bytes_per_sample;
size_t ring_buffer_size =
fbl::round_up<size_t, size_t>(min_frames * frame_size_ + aml_audio_->fifo_depth(),
std::lcm(frame_size_, aml_audio_->GetBufferAlignment()));
size_t out_frames = ring_buffer_size / frame_size_;
if (out_frames > std::numeric_limits<uint32_t>::max()) {
zxlogf(ERROR, "out frames too big %zu", out_frames);
ringbuffer_binding_->Unbind();
return;
}
auto status = InitBuffer(ring_buffer_size);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to init buffer %d", status);
ringbuffer_binding_->Unbind();
return;
}
constexpr uint32_t rights = ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER;
zx::vmo buffer;
status = ring_buffer_vmo_.duplicate(rights, &buffer);
if (status != ZX_OK) {
zxlogf(ERROR, "GetVmo failed, could not duplicate VMO");
ringbuffer_binding_->Unbind();
return;
}
status = aml_audio_->SetBuffer(pinned_ring_buffer_.region(0).phys_addr, ring_buffer_size);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to set buffer %d", status);
ringbuffer_binding_->Unbind();
return;
}
expected_notifications_per_ring_.store(clock_recovery_notifications_per_ring);
rb_fetched_ = true;
// This is safe because of the overflow check we made above.
auto out_num_rb_frames = static_cast<uint32_t>(out_frames);
callback(fpromise::ok(std::make_tuple(out_num_rb_frames, std::move(buffer))));
}
void AmlG12TdmDai::Start(StartCallback callback) {
uint64_t start_time = 0;
if (rb_started_) {
zxlogf(ERROR, "Could not start: already started");
ringbuffer_binding_->Close(ZX_ERR_BAD_STATE);
return;
}
if (!rb_fetched_) {
zxlogf(ERROR, "Could not start: first, GetVmo must successfully complete");
ringbuffer_binding_->Close(ZX_ERR_BAD_STATE);
return;
}
start_time = aml_audio_->Start();
rb_started_ = true;
uint32_t notifs = expected_notifications_per_ring_.load();
if (notifs) {
us_per_notification_ =
static_cast<uint32_t>(1000 * pinned_ring_buffer_.region(0).size /
(frame_size_ * dai_format_.frame_rate / 1000 * notifs));
notify_timer_.PostDelayed(loop_.dispatcher(), zx::usec(us_per_notification_));
} else {
us_per_notification_ = 0;
}
callback(start_time);
}
void AmlG12TdmDai::Stop(StopCallback callback) {
if (!rb_fetched_) {
zxlogf(ERROR, "GetVmo must successfully complete before calling Start or Stop");
ringbuffer_binding_->Close(ZX_ERR_BAD_STATE);
return;
}
if (!rb_started_) {
zxlogf(INFO, "Stop called while stopped; this is allowed");
}
notify_timer_.Cancel();
us_per_notification_ = 0;
aml_audio_->Stop();
rb_started_ = false;
callback();
}
zx_status_t AmlG12TdmDai::InitBuffer(size_t size) {
// Make sure the DMA is stopped before releasing quarantine.
aml_audio_->Stop();
// Make sure that all reads/writes have gone through.
#if defined(__aarch64__)
__asm__ volatile("dsb sy" : : : "memory");
#endif
auto status = bti_.release_quarantine();
if (status != ZX_OK) {
zxlogf(ERROR, "could not release quarantine bti - %d", status);
return status;
}
pinned_ring_buffer_.Unpin();
status = zx_vmo_create_contiguous(bti_.get(), size, 0, ring_buffer_vmo_.reset_and_get_address());
if (status != ZX_OK) {
zxlogf(ERROR, "failed to allocate ring buffer vmo - %d", status);
return status;
}
status = pinned_ring_buffer_.Pin(ring_buffer_vmo_, bti_, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to pin ring buffer vmo - %d", status);
return status;
}
if (pinned_ring_buffer_.region_count() != 1) {
if (!AllowNonContiguousRingBuffer()) {
zxlogf(ERROR, "buffer is not contiguous");
return ZX_ERR_NO_MEMORY;
}
}
return ZX_OK;
}
void AmlG12TdmDai::GetProperties(::fuchsia::hardware::audio::Dai::GetPropertiesCallback callback) {
::fuchsia::hardware::audio::DaiProperties props;
props.set_is_input(metadata_.is_input);
props.set_manufacturer(metadata_.manufacturer);
props.set_product_name(metadata_.product_name);
props.set_clock_domain(::fuchsia::hardware::audio::CLOCK_DOMAIN_MONOTONIC);
callback(std::move(props));
}
void AmlG12TdmDai::GetRingBufferFormats(GetRingBufferFormatsCallback callback) {
::fuchsia::hardware::audio::Dai_GetRingBufferFormats_Result result;
::fuchsia::hardware::audio::Dai_GetRingBufferFormats_Response response;
::fuchsia::hardware::audio::PcmSupportedFormats pcm_formats;
::fuchsia::hardware::audio::ChannelSet channel_set;
std::vector<::fuchsia::hardware::audio::ChannelAttributes> attributes(
metadata_.ring_buffer.number_of_channels);
channel_set.set_attributes(std::move(attributes));
pcm_formats.mutable_channel_sets()->push_back(std::move(channel_set));
pcm_formats.mutable_sample_formats()->push_back(
::fuchsia::hardware::audio::SampleFormat::PCM_SIGNED);
pcm_formats.mutable_bytes_per_sample()->push_back(metadata_.ring_buffer.bytes_per_sample);
pcm_formats.mutable_valid_bits_per_sample()->push_back(metadata_.ring_buffer.bytes_per_sample *
8);
pcm_formats.set_frame_rates(AmlTdmConfigDevice::GetSupportedFrameRates());
::fuchsia::hardware::audio::SupportedFormats formats;
formats.set_pcm_supported_formats(std::move(pcm_formats));
response.ring_buffer_formats.push_back(std::move(formats));
result.set_response(std::move(response));
callback(std::move(result));
}
void AmlG12TdmDai::GetDaiFormats(GetDaiFormatsCallback callback) {
::fuchsia::hardware::audio::Dai_GetDaiFormats_Result result;
::fuchsia::hardware::audio::Dai_GetDaiFormats_Response response;
::fuchsia::hardware::audio::DaiSupportedFormats formats;
formats.number_of_channels.push_back(metadata_.dai.number_of_channels);
formats.sample_formats.push_back(::fuchsia::hardware::audio::DaiSampleFormat::PCM_SIGNED);
::fuchsia::hardware::audio::DaiFrameFormat frame_format;
switch (metadata_.dai.type) {
case metadata::DaiType::I2s:
frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::I2S);
break;
case metadata::DaiType::StereoLeftJustified:
frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::STEREO_LEFT);
break;
case metadata::DaiType::Tdm1:
frame_format.set_frame_format_standard(
::fuchsia::hardware::audio::DaiFrameFormatStandard::TDM1);
break;
default:
ZX_ASSERT(0); // Not supported.
}
formats.frame_formats.push_back(std::move(frame_format));
formats.frame_rates = AmlTdmConfigDevice::GetSupportedFrameRates();
formats.bits_per_slot.push_back(metadata_.dai.bits_per_slot);
formats.bits_per_sample.push_back(metadata_.dai.bits_per_sample);
response.dai_formats.push_back(std::move(formats));
result.set_response(std::move(response));
callback(std::move(result));
}
void AmlG12TdmDai::CreateRingBuffer(
::fuchsia::hardware::audio::DaiFormat dai_format,
::fuchsia::hardware::audio::Format ring_buffer_format,
::fidl::InterfaceRequest<::fuchsia::hardware::audio::RingBuffer> ring_buffer) {
if (ring_buffer_format.pcm_format().frame_rate == 0) {
zxlogf(ERROR, "Bad (zero) frame rate");
ring_buffer.Close(ZX_ERR_INVALID_ARGS);
return;
}
uint32_t bytes_per_frame = ring_buffer_format.pcm_format().bytes_per_sample *
ring_buffer_format.pcm_format().number_of_channels;
if (bytes_per_frame == 0) {
zxlogf(ERROR, "Bad (zero) bytes per frame");
ring_buffer.Close(ZX_ERR_INVALID_ARGS);
return;
}
// Stop and terminate a previous ring buffer.
if (rb_started_) {
Stop([]() {});
ringbuffer_binding_->Unbind();
}
ringbuffer_binding_.emplace(this, std::move(ring_buffer), loop_.dispatcher());
// Clear delay info sent state such that a call to WatchDelayInfo after CreateRingBuffer will be
// replied to the first time. Doing it here instead of in the error handler guarantees that once
// CreateRingBuffer is called, a WatchDelayInfo will be replied to even before the error handler
// is called for any previous ring buffer in use.
delay_info_sent_ = false;
ringbuffer_binding_->set_error_handler([this](zx_status_t status) -> void {
zxlogf(INFO, "RingBuffer protocol %s", zx_status_get_string(status));
ResetRingBuffer();
});
dai_format_ = std::move(dai_format);
internal_delay_nsec_ = 0; // No internal delay known, so we report 0.
Reset([]() {});
}
void AmlG12TdmDai::ResetRingBuffer() {
rb_fetched_ = false;
rb_started_ = false;
expected_notifications_per_ring_ = 0;
position_callback_.reset();
dai_format_ = {};
Stop([]() {});
}
void AmlG12TdmDai::GetProperties(
::fuchsia::hardware::audio::RingBuffer::GetPropertiesCallback callback) {
::fuchsia::hardware::audio::RingBufferProperties prop;
prop.set_driver_transfer_bytes(aml_audio_->fifo_depth())
.set_needs_cache_flush_or_invalidate(true);
callback(std::move(prop));
}
void AmlG12TdmDai::ProcessRingNotification() {
if (us_per_notification_) {
notify_timer_.PostDelayed(loop_.dispatcher(), zx::usec(us_per_notification_));
} else {
notify_timer_.Cancel();
return;
}
::fuchsia::hardware::audio::RingBufferPositionInfo info;
info.position = aml_audio_->GetRingPosition();
info.timestamp = zx::clock::get_monotonic().get();
if (position_callback_) {
(*position_callback_)(std::move(info));
position_callback_.reset();
}
}
void AmlG12TdmDai::WatchClockRecoveryPositionInfo(WatchClockRecoveryPositionInfoCallback callback) {
if (!expected_notifications_per_ring_.load()) {
zxlogf(ERROR, "no notifications per ring");
}
position_callback_ = std::move(callback);
}
void AmlG12TdmDai::WatchDelayInfo(WatchDelayInfoCallback callback) {
if (delay_info_sent_) {
return; // Only send delay state once, as if it never changed.
}
delay_info_sent_ = true;
fuchsia::hardware::audio::DelayInfo delay_info = {};
// No external delay information is provided by this driver.
delay_info.set_internal_delay(internal_delay_nsec_);
callback(std::move(delay_info));
}
static zx_status_t dai_bind(void* ctx, zx_device_t* device) {
size_t actual = 0;
metadata::AmlConfig metadata = {};
auto status = device_get_fragment_metadata(device, "pdev", DEVICE_METADATA_PRIVATE, &metadata,
sizeof(metadata::AmlConfig), &actual);
if (status != ZX_OK || sizeof(metadata::AmlConfig) != actual) {
zxlogf(ERROR, "device_get_fragment_metadata failed %d", status);
return status;
}
ddk::PDevFidl pdev = ddk::PDevFidl::FromFragment(device);
if (!pdev.is_valid()) {
zxlogf(ERROR, "could not get pdev");
return ZX_ERR_NO_RESOURCES;
}
auto dai = std::make_unique<audio::aml_g12::AmlG12TdmDai>(device, std::move(pdev));
if (dai == nullptr) {
zxlogf(ERROR, "Could not create DAI driver");
return ZX_ERR_NO_MEMORY;
}
status = dai->InitPDev();
if (status != ZX_OK) {
zxlogf(ERROR, "Could not init device");
return status;
}
zx_device_prop_t props[] = {
{BIND_PLATFORM_DEV_VID, 0, PDEV_VID_AMLOGIC},
{BIND_PLATFORM_DEV_DID, 0, PDEV_DID_AMLOGIC_DAI_OUT},
};
const char* name = "aml-g12-tdm-dai-out";
if (metadata.is_input) {
props[1].value = PDEV_DID_AMLOGIC_DAI_IN;
name = "aml-g12-tdm-dai-in";
}
status = dai->DdkAdd(ddk::DeviceAddArgs(name).set_props(props));
if (status != ZX_OK) {
zxlogf(ERROR, "Could not add DAI driver to the DDK");
return status;
}
[[maybe_unused]] auto unused = dai.release();
return ZX_OK;
}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = dai_bind;
return ops;
}();
} // namespace audio::aml_g12
// clang-format off
ZIRCON_DRIVER(aml_g12_tdm_dai, audio::aml_g12::driver_ops, "aml-g12-tdm-dai", "0.1");