| // Copyright 2018 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 <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/platform-defs.h> |
| #include <ddk/protocol/composite.h> |
| #include <ddktl/pdev.h> |
| #include <soc/aml-t931/t931-gpio.h> |
| |
| #include "audio-stream-out.h" |
| |
| namespace audio { |
| namespace sherlock { |
| |
| enum { |
| COMPONENT_PDEV, |
| COMPONENT_FAULT_GPIO, |
| COMPONENT_ENABLE_GPIO, |
| COMPONENT_I2C_0, |
| COMPONENT_I2C_1, |
| COMPONENT_I2C_2, // Optional |
| COMPONENT_COUNT, |
| }; |
| |
| // Expects L+R for tweeters + L+R for the 1 Woofer (mixed in HW). |
| // The user must perform crossover filtering on these channels. |
| constexpr size_t kNumberOfChannels = 4; |
| // Calculate ring buffer size for 1 second of 16-bit, 48kHz. |
| constexpr size_t kRingBufferSize = fbl::round_up<size_t, size_t>(48000 * 2 * kNumberOfChannels, |
| PAGE_SIZE); |
| |
| SherlockAudioStreamOut::SherlockAudioStreamOut(zx_device_t* parent) |
| : SimpleAudioStream(parent, false), pdev_(parent) { |
| } |
| |
| zx_status_t SherlockAudioStreamOut::InitPdev() { |
| composite_protocol_t composite; |
| |
| auto status = device_get_protocol(parent(), ZX_PROTOCOL_COMPOSITE, &composite); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Could not get composite protocol\n"); |
| return status; |
| } |
| |
| zx_device_t* components[COMPONENT_COUNT] = {}; |
| size_t actual; |
| composite_get_components(&composite, components, countof(components), &actual); |
| if (actual < countof(components) - 1) { |
| zxlogf(ERROR, "could not get components\n"); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| pdev_ = components[COMPONENT_PDEV]; |
| if (!pdev_.is_valid()) { |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| status = device_get_metadata(parent(), DEVICE_METADATA_PRIVATE, &codecs_types_, |
| sizeof(metadata::Codec), &actual); |
| if (status != ZX_OK || sizeof(metadata::Codec) != actual) { |
| zxlogf(ERROR, "%s device_get_metadata failed %d\n", __FILE__, status); |
| return status; |
| } |
| |
| if (codecs_types_ == metadata::Codec::Tas5760_Tas5720) { |
| zxlogf(INFO, "audio: using Tas5760 and Tas5720 codecs\n"); |
| fbl::AllocChecker ac; |
| codecs_ = fbl::Array(new (&ac) fbl::unique_ptr<Codec>[2], 2); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| codecs_[0] = Tas5760::Create(components[COMPONENT_I2C_0]); |
| if (!codecs_[0]) { |
| zxlogf(ERROR, "%s could not get tas5760\n", __func__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| codecs_[1] = Tas5720::Create(components[COMPONENT_I2C_1]); |
| if (!codecs_[1]) { |
| zxlogf(ERROR, "%s could not get tas5720\n", __func__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| } else if (codecs_types_ == metadata::Codec::Tas5720x3) { |
| zxlogf(INFO, "audio: using 3 Tas5720 codecs\n"); |
| fbl::AllocChecker ac; |
| codecs_ = fbl::Array(new (&ac) fbl::unique_ptr<Codec>[3], 3); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| for (uint32_t i = 0; i < 3; ++i) { |
| codecs_[i] = Tas5720::Create(components[COMPONENT_I2C_0 + i]); |
| if (!codecs_[i]) { |
| zxlogf(ERROR, "%s could not get tas5720\n", __func__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| } |
| } else { |
| zxlogf(ERROR, "%s invalid or unsupported codec\n", __func__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| audio_fault_ = components[COMPONENT_FAULT_GPIO]; |
| audio_en_ = components[COMPONENT_ENABLE_GPIO]; |
| |
| if (!audio_fault_.is_valid() || !audio_en_.is_valid()) { |
| zxlogf(ERROR, "%s failed to allocate gpio\n", __func__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| status = pdev_.GetBti(0, &bti_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not obtain bti - %d\n", __func__, status); |
| return status; |
| } |
| |
| std::optional<ddk::MmioBuffer> mmio; |
| status = pdev_.MapMmio(0, &mmio); |
| if (status != ZX_OK) { |
| return status; |
| } |
| aml_audio_ = AmlTdmDevice::Create(*std::move(mmio), HIFI_PLL, TDM_OUT_C, FRDDR_A, MCLK_C); |
| if (aml_audio_ == nullptr) { |
| zxlogf(ERROR, "%s failed to create tdm device\n", __func__); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| // Drive strength settings |
| status = pdev_.MapMmio(1, &mmio); |
| if (status != ZX_OK) { |
| return status; |
| } |
| // Strength 1 for sclk (bit 14, GPIOZ(7)) and lrclk (bit 12, GPIOZ(6)), |
| // GPIO offsets are in 4 bytes units. |
| mmio->SetBits<uint32_t>((1 << 14) | (1 << 12), 4 * T931_PAD_DS_REG4A); |
| status = pdev_.MapMmio(2, &mmio); |
| if (status != ZX_OK) { |
| return status; |
| } |
| // Strength 1 for mclk (bit 18, GPIOAO(9)), GPIO offsets are in 4 bytes units. |
| mmio->SetBit<uint32_t>(18, 4 * T931_AO_PAD_DS_A); |
| |
| audio_en_.Write(1); // SOC_AUDIO_EN. |
| |
| if (codecs_types_ == metadata::Codec::Tas5760_Tas5720) { |
| codecs_[0]->Init(std::nullopt); // No slot setting, always uses L+R. |
| codecs_[1]->Init(0); // Use TDM slot 0. |
| } else { |
| codecs_[0]->Init(0); // Use TDM slot 0. |
| codecs_[1]->Init(1); // Use TDM slot 1. |
| codecs_[2]->Init(0); // Use TDM slot 0. |
| } |
| |
| InitBuffer(kRingBufferSize); |
| |
| aml_audio_->SetBuffer(pinned_ring_buffer_.region(0).phys_addr, |
| pinned_ring_buffer_.region(0).size); |
| |
| // Setup Stereo Left Justified: |
| // -lrclk duty = 64 sclk (SetSclkDiv lrdiv=63 below). |
| // -No delay from the time the lrclk signal changes state state to the first bit of data on the |
| // data lines (ConfigTdmOutSlot bitoffset=4 below accomplishes this). |
| // -3072MHz/64 = 48KHz. |
| |
| // 4 bitoffset, 2 slots, 32 bits/slot, 16 bits/sample, enable mix L+R on lane 1. |
| aml_audio_->ConfigTdmOutSlot(4, 1, 31, 15, (1 << 1)); |
| |
| // Lane 0 L channel set to FRDDR slot 0. |
| // Lane 0 R channel set to FRDDR slot 1. |
| // Lane 1 L channel set to FRDDR slot 2. Mixed with R, see ConfigTdmOutSlot above. |
| // Lane 1 R channel set to FRDDR slot 3. Mixed with L, see ConfigTdmOutSlot above. |
| aml_audio_->ConfigTdmOutSwaps(0x00003210); |
| |
| // Tweeters: Lane 0, unmask TDM slots 0 & 1 (L+R FRDDR slots 0 & 1). |
| aml_audio_->ConfigTdmOutLane(0, 0x00000003); |
| |
| // Woofer: Lane 1, unmask TDM slot 0 & 1 (Woofer FRDDR slots 2 & 3). |
| aml_audio_->ConfigTdmOutLane(1, 0x00000003); |
| |
| // mclk = T931_HIFI_PLL_RATE/125 = 1536MHz/125 = 12.288MHz. |
| aml_audio_->SetMclkDiv(124); |
| |
| // Per schematic, mclk uses pad 0 (MCLK_0 instead of MCLK_1). |
| aml_audio_->SetMClkPad(MCLK_PAD_0); |
| |
| // sclk = 12.288MHz/4 = 3.072MHz, 32L + 32R sclks = 64 sclks. |
| aml_audio_->SetSclkDiv(3, 31, 63); |
| |
| aml_audio_->Sync(); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::Init() { |
| zx_status_t status; |
| |
| status = InitPdev(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = AddFormats(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| float gain = codecs_[0]->GetGain(); |
| float min_gain = codecs_[0]->GetMinGain(); |
| float max_gain = codecs_[0]->GetMaxGain(); |
| float gain_step = codecs_[0]->GetGainStep(); |
| for (size_t i = 1; i < codecs_.size(); ++i) { |
| min_gain = fbl::max(min_gain, codecs_[i]->GetMinGain()); |
| max_gain = fbl::min(max_gain, codecs_[i]->GetMaxGain()); |
| gain_step = fbl::max(gain_step, codecs_[i]->GetGainStep()); |
| status = codecs_[i]->SetGain(gain); |
| if (status != ZX_OK) { |
| return status; |
| } |
| } |
| cur_gain_state_.cur_gain = gain; |
| cur_gain_state_.cur_mute = false; |
| cur_gain_state_.cur_agc = false; |
| |
| cur_gain_state_.min_gain = min_gain; |
| cur_gain_state_.max_gain = max_gain; |
| cur_gain_state_.gain_step = gain_step; |
| cur_gain_state_.can_mute = false; |
| cur_gain_state_.can_agc = false; |
| |
| snprintf(device_name_, sizeof(device_name_), "sherlock-audio-out"); |
| snprintf(mfr_name_, sizeof(mfr_name_), "unknown"); |
| snprintf(prod_name_, sizeof(prod_name_), "sherlock"); |
| |
| unique_id_ = AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS; |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::InitPost() { |
| |
| notify_timer_ = dispatcher::Timer::Create(); |
| if (notify_timer_ == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| dispatcher::Timer::ProcessHandler thandler( |
| [tdm = this](dispatcher::Timer * timer)->zx_status_t { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(t, tdm->domain_); |
| return tdm->ProcessRingNotification(); |
| }); |
| |
| return notify_timer_->Activate(domain_, std::move(thandler)); |
| } |
| |
| // Timer handler for sending out position notifications. |
| zx_status_t SherlockAudioStreamOut::ProcessRingNotification() { |
| ZX_ASSERT(us_per_notification_ != 0); |
| |
| // TODO(andresoportus): johngro noticed there is some drifting on notifications here, |
| // could be improved with maintaining an absolute time and even better computing using |
| // rationals, but higher level code should not rely on this anyways (see MTWN-57). |
| notify_timer_->Arm(zx_deadline_after(ZX_USEC(us_per_notification_))); |
| |
| audio_proto::RingBufPositionNotify resp = {}; |
| resp.hdr.cmd = AUDIO_RB_POSITION_NOTIFY; |
| |
| resp.ring_buffer_pos = aml_audio_->GetRingPosition(); |
| return NotifyPosition(resp); |
| } |
| |
| zx_status_t SherlockAudioStreamOut::ChangeFormat(const audio_proto::StreamSetFmtReq& req) { |
| fifo_depth_ = aml_audio_->fifo_depth(); |
| external_delay_nsec_ = 0; |
| |
| // At this time only one format is supported, and hardware is initialized |
| // during driver binding, so nothing to do at this time. |
| return ZX_OK; |
| } |
| |
| void SherlockAudioStreamOut::ShutdownHook() { |
| aml_audio_->Shutdown(); |
| audio_en_.Write(0); |
| } |
| |
| zx_status_t SherlockAudioStreamOut::SetGain(const audio_proto::SetGainReq& req) { |
| for (size_t i = 0; i < codecs_.size(); ++i) { |
| zx_status_t status = codecs_[i]->SetGain(req.gain); |
| if (status != ZX_OK) { |
| return status; |
| } |
| } |
| cur_gain_state_.cur_gain = req.gain; |
| // TODO(andresoportus): More options on volume setting, e.g.: |
| // -Allow for ratio between tweeters and woofer gains. |
| // -Make use of analog gain options in TAS5720. |
| // -Add codecs mute and fade support. |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::GetBuffer(const audio_proto::RingBufGetBufferReq& req, |
| uint32_t* out_num_rb_frames, |
| zx::vmo* out_buffer) { |
| |
| uint32_t rb_frames = |
| static_cast<uint32_t>(pinned_ring_buffer_.region(0).size) / frame_size_; |
| |
| if (req.min_ring_buffer_frames > rb_frames) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| zx_status_t status; |
| constexpr uint32_t rights = ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER; |
| status = ring_buffer_vmo_.duplicate(rights, out_buffer); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| *out_num_rb_frames = rb_frames; |
| |
| aml_audio_->SetBuffer(pinned_ring_buffer_.region(0).phys_addr, |
| rb_frames * frame_size_); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::Start(uint64_t* out_start_time) { |
| |
| *out_start_time = aml_audio_->Start(); |
| |
| uint32_t notifs = LoadNotificationsPerRing(); |
| if (notifs) { |
| us_per_notification_ = static_cast<uint32_t>( |
| 1000 * pinned_ring_buffer_.region(0).size / (frame_size_ * 48 * notifs)); |
| notify_timer_->Arm(zx_deadline_after(ZX_USEC(us_per_notification_))); |
| } else { |
| us_per_notification_ = 0; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::Stop() { |
| notify_timer_->Cancel(); |
| us_per_notification_ = 0; |
| aml_audio_->Stop(); |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::AddFormats() { |
| fbl::AllocChecker ac; |
| supported_formats_.reserve(1, &ac); |
| if (!ac.check()) { |
| zxlogf(ERROR, "Out of memory, can not create supported formats list\n"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| // Add the range for basic audio support. |
| audio_stream_format_range_t range; |
| |
| range.min_channels = kNumberOfChannels; |
| range.max_channels = kNumberOfChannels; |
| range.sample_formats = AUDIO_SAMPLE_FORMAT_16BIT; |
| range.min_frames_per_second = 48000; |
| range.max_frames_per_second = 48000; |
| range.flags = ASF_RANGE_FLAG_FPS_48000_FAMILY; |
| |
| supported_formats_.push_back(range); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t SherlockAudioStreamOut::InitBuffer(size_t size) { |
| zx_status_t status; |
| // TODO(ZX-3149): Per johngro's suggestion preallocate contiguous memory (say in |
| // platform bus) since we are likely to fail after running for a while and we need to |
| // init again (say the devhost is restarted). |
| status = zx_vmo_create_contiguous(bti_.get(), size, 0, |
| ring_buffer_vmo_.reset_and_get_address()); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to allocate ring buffer vmo - %d\n", __func__, 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, "%s failed to pin ring buffer vmo - %d\n", __func__, status); |
| return status; |
| } |
| if (pinned_ring_buffer_.region_count() != 1) { |
| zxlogf(ERROR, "%s buffer is not contiguous", __func__); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| return ZX_OK; |
| } |
| |
| static zx_status_t audio_bind(void* ctx, zx_device_t* device) { |
| auto stream = |
| audio::SimpleAudioStream::Create<audio::sherlock::SherlockAudioStreamOut>(device); |
| if (stream == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| return ZX_OK; |
| } |
| |
| static zx_driver_ops_t driver_ops = [](){ |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = audio_bind; |
| return ops; |
| }(); |
| |
| } // sherlock |
| } // audio |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(aml_sherlock_tdm, audio::sherlock::driver_ops, "zircon", "0.1", 4) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_COMPOSITE), |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_AMLOGIC), |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_AMLOGIC_T931), |
| BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_AMLOGIC_TDM), |
| ZIRCON_DRIVER_END(aml_sherlock_tdm) |
| // clang-format on |
| |