| // 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/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> |
| |
| #include "src/media/audio/drivers/aml-g12-tdm/aml_tdm_dai_bind.h" |
| |
| namespace audio::aml_g12 { |
| |
| enum { |
| FRAGMENT_PDEV, |
| FRAGMENT_COUNT, |
| }; |
| |
| AmlG12TdmDai::AmlG12TdmDai(zx_device_t* parent, ddk::PDev 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::kSupportedFrameRates[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; |
| default: |
| ZX_ASSERT(0); // Not supported. |
| } |
| } |
| |
| 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([]() {}); |
| }); |
| } |
| |
| 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_metadata(parent(), DEVICE_METADATA_PRIVATE, &metadata_, |
| sizeof(metadata::AmlConfig), &actual); |
| if (status != ZX_OK || sizeof(metadata::AmlConfig) != actual) { |
| zxlogf(ERROR, "device_get_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_, 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_ || !rb_fetched_) { |
| zxlogf(ERROR, "Could not start"); |
| callback(start_time); |
| 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_started_) { |
| zxlogf(ERROR, "Could not stop"); |
| callback(); |
| return; |
| } |
| 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); |
| 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); |
| for (size_t i = 0; i < std::size(AmlTdmConfigDevice::kSupportedFrameRates); ++i) { |
| pcm_formats.mutable_frame_rates()->push_back(AmlTdmConfigDevice::kSupportedFrameRates[i]); |
| } |
| ::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)); |
| for (size_t i = 0; i < std::size(AmlTdmConfigDevice::kSupportedFrameRates); ++i) { |
| formats.frame_rates.push_back(AmlTdmConfigDevice::kSupportedFrameRates[i]); |
| } |
| 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) { |
| // Stop and terminate a previous ring buffer. |
| if (rb_started_) { |
| Stop([]() {}); |
| ringbuffer_binding_->Unbind(); |
| } |
| |
| ringbuffer_binding_.emplace(this, std::move(ring_buffer), loop_.dispatcher()); |
| ringbuffer_binding_->set_error_handler([this](zx_status_t status) -> void { |
| zxlogf(INFO, "RingBuffer protocol %s", zx_status_get_string(status)); |
| Stop([]() {}); |
| }); |
| dai_format_ = std::move(dai_format); |
| Reset([]() {}); |
| } |
| |
| void AmlG12TdmDai::GetProperties( |
| ::fuchsia::hardware::audio::RingBuffer::GetPropertiesCallback callback) { |
| ::fuchsia::hardware::audio::RingBufferProperties prop; |
| prop.set_external_delay(0); |
| prop.set_fifo_depth(aml_audio_->fifo_depth()); |
| prop.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); |
| } |
| |
| static zx_status_t dai_bind(void* ctx, zx_device_t* device) { |
| size_t actual = 0; |
| metadata::AmlConfig metadata = {}; |
| auto status = device_get_metadata(device, DEVICE_METADATA_PRIVATE, &metadata, |
| sizeof(metadata::AmlConfig), &actual); |
| if (status != ZX_OK || sizeof(metadata::AmlConfig) != actual) { |
| zxlogf(ERROR, "device_get_metadata failed %d", status); |
| return status; |
| } |
| pdev_protocol_t pdev; |
| status = device_get_protocol(device, ZX_PROTOCOL_PDEV, &pdev); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "could not get pdev %d", status); |
| return status; |
| } |
| auto dai = std::make_unique<audio::aml_g12::AmlG12TdmDai>(device, ddk::PDev(&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"); |