| // 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 "audio-stream-out.h" |
| |
| #include <lib/mmio/mmio.h> |
| #include <lib/zx/clock.h> |
| |
| #include <optional> |
| #include <utility> |
| |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/driver.h> |
| #include <ddk/metadata.h> |
| #include <ddk/platform-defs.h> |
| #include <ddk/protocol/composite.h> |
| #include <ddktl/metadata/audio.h> |
| #include <fbl/array.h> |
| |
| // TODO(andresoportus): Add handling for the other formats supported by this controller. |
| |
| namespace { |
| |
| enum { |
| FRAGMENT_PDEV, |
| FRAGMENT_CODEC, |
| FRAGMENT_CLOCK, |
| FRAGMENT_COUNT, |
| }; |
| |
| } // namespace |
| |
| namespace audio { |
| namespace nelson { |
| |
| // Expects L+R. |
| constexpr size_t kNumberOfChannels = 2; |
| |
| NelsonAudioStreamOut::NelsonAudioStreamOut(zx_device_t* parent) |
| : SimpleAudioStream(parent, false), pdev_(parent) {} |
| |
| zx_status_t NelsonAudioStreamOut::InitCodec() { |
| auto status = codec_.GetInfo(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not get codec info %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| // Reset and initialize codec after we have configured I2S. |
| status = codec_.Reset(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not reset codec %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| status = codec_.SetNotBridged(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not set not bridged mode %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| status = codec_.CheckExpectedDaiFormat(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not get expected DAI format %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| uint32_t channels[] = {0, 1}; |
| dai_format_t format = { |
| .number_of_channels = 2, |
| .channels_to_use_list = channels, |
| .channels_to_use_count = countof(channels), |
| .sample_format = kWantedSampleFormat, |
| .justify_format = kWantedJustifyFormat, |
| .frame_rate = kWantedFrameRate, |
| .bits_per_channel = kWantedBitsPerChannel, |
| .bits_per_sample = kWantedBitsPerSample, |
| }; |
| status = codec_.SetDaiFormat(format); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not set DAI format %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::InitHW() { |
| lib_->Shutdown(); |
| |
| lib_->Initialize(); |
| |
| // Setup TDM. |
| |
| // 3 bitoffset, 2 slots, 32 bits/slot, 16 bits/sample (works for 32 in codec), no mixing. |
| lib_->ConfigTdmOutSlot(3, 1, 31, 15, 0); |
| |
| // Lane0 right channel. |
| lib_->ConfigTdmOutSwaps(0x00000010); |
| |
| // Lane 0, unmask first 2 slots (0x00000003), |
| auto status = lib_->ConfigTdmOutLane(0, 0x00000003, 0); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not configure TDM out lane %d", __FILE__, status); |
| return status; |
| } |
| |
| // Setup appropriate tdm clock signals. mclk = 1.536GHz/125 = 12.288MHz. |
| status = lib_->SetMclkDiv(124); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not configure MCLK %d", __FILE__, status); |
| return status; |
| } |
| |
| // sclk = 12.288MHz/4 = 3.072MHz, 32 every 64 sclks is frame sync (I2S). |
| status = lib_->SetSclkDiv(3, 31, 63, true); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not configure SCLK %d", __FILE__, status); |
| return status; |
| } |
| |
| lib_->Sync(); |
| |
| status = InitCodec(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not initialize codec - %d", __FILE__, status); |
| return status; |
| } |
| |
| zxlogf(INFO, "audio: Nelson audio output initialized"); |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::InitPdev() { |
| composite_protocol_t composite; |
| |
| auto status = device_get_protocol(parent(), ZX_PROTOCOL_COMPOSITE, &composite); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s Could not get composite protocol", __FILE__); |
| return status; |
| } |
| |
| zx_device_t* fragments[FRAGMENT_COUNT] = {}; |
| size_t actual; |
| composite_get_fragments(&composite, fragments, countof(fragments), &actual); |
| if (actual != FRAGMENT_COUNT) { |
| zxlogf(ERROR, "%s could not get fragments", __FILE__); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| pdev_ = fragments[FRAGMENT_PDEV]; |
| if (!pdev_.is_valid()) { |
| zxlogf(ERROR, "%s could not get pdev", __FILE__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| codec_.proto_client_ = fragments[FRAGMENT_CODEC]; |
| if (!pdev_.is_valid()) { |
| zxlogf(ERROR, "%s Could not get pdev", __FILE__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| status = pdev_.GetBti(0, &bti_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not obtain bti %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| clks_[kHifiPllClk] = fragments[FRAGMENT_CLOCK]; |
| if (!clks_[kHifiPllClk].is_valid()) { |
| zxlogf(ERROR, "%s GetClk failed", __FILE__); |
| return status; |
| } |
| |
| // HIFI_PLL = 1.536GHz = 125 * 4 * 64 * 48000 (kWantedFrameRate) |
| clks_[kHifiPllClk].SetRate(125 * 4 * 64 * kWantedFrameRate); |
| clks_[kHifiPllClk].Enable(); |
| |
| std::optional<ddk::MmioBuffer> mmio; |
| status = pdev_.MapMmio(0, &mmio); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not map mmio %d", __FUNCTION__, status); |
| return status; |
| } |
| |
| lib_ = AmlTdmDevice::Create(*std::move(mmio), HIFI_PLL, TDM_OUT_B, FRDDR_B, MCLK_B, |
| AmlVersion::kS905D3G); |
| if (lib_ == nullptr) { |
| zxlogf(ERROR, "%s failed to create audio device", __FILE__); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| // Calculate ring buffer size for 1 second of 16-bit at max rate. |
| const size_t kRingBufferSize = fbl::round_up<size_t, size_t>( |
| kWantedFrameRate * sizeof(uint16_t) * kNumberOfChannels, ZX_PAGE_SIZE); |
| status = InitBuffer(kRingBufferSize); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to Init buffer %d", __FILE__, status); |
| return status; |
| } |
| |
| lib_->SetBuffer(pinned_ring_buffer_.region(0).phys_addr, pinned_ring_buffer_.region(0).size); |
| |
| codec_.proto_client_ = fragments[FRAGMENT_CODEC]; |
| if (!codec_.proto_client_.is_valid()) { |
| zxlogf(ERROR, "%s Could not get codec", __FILE__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| return InitHW(); |
| } |
| |
| zx_status_t NelsonAudioStreamOut::Init() { |
| auto status = InitPdev(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = AddFormats(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not add formats %d", __FILE__, status); |
| return status; |
| } |
| |
| // Get our gain capabilities. |
| gain_state_t state = {}; |
| status = codec_.GetGainState(&state); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not get gain state %d", __FILE__, status); |
| return status; |
| } |
| cur_gain_state_.cur_gain = state.gain; |
| cur_gain_state_.cur_mute = state.muted; |
| cur_gain_state_.cur_agc = state.agc_enable; |
| |
| gain_format_t format = {}; |
| status = codec_.GetGainFormat(&format); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s could not get gain format %d", __FILE__, status); |
| return status; |
| } |
| |
| cur_gain_state_.min_gain = format.min_gain; |
| cur_gain_state_.max_gain = format.max_gain; |
| cur_gain_state_.gain_step = format.gain_step; |
| cur_gain_state_.can_mute = format.can_mute; |
| cur_gain_state_.can_agc = format.can_agc; |
| |
| snprintf(device_name_, sizeof(device_name_), "nelson-audio-out"); |
| snprintf(mfr_name_, sizeof(mfr_name_), "unknown"); |
| snprintf(prod_name_, sizeof(prod_name_), "nelson"); |
| |
| unique_id_ = AUDIO_STREAM_UNIQUE_ID_BUILTIN_SPEAKERS; |
| |
| // TODO(mpuryear): change this to the domain of the clock received from the board driver |
| clock_domain_ = 0; |
| |
| return ZX_OK; |
| } |
| |
| // Timer handler for sending out position notifications. |
| void NelsonAudioStreamOut::ProcessRingNotification() { |
| ScopedToken t(domain_token()); |
| ZX_ASSERT(us_per_notification_ != 0); |
| |
| ZX_ASSERT(notify_timer_.PostDelayed(dispatcher(), zx::usec(us_per_notification_)) == ZX_OK); |
| |
| audio_proto::RingBufPositionNotify resp = {}; |
| resp.hdr.cmd = AUDIO_RB_POSITION_NOTIFY; |
| |
| resp.monotonic_time = zx::clock::get_monotonic().get(); |
| resp.ring_buffer_pos = lib_->GetRingPosition(); |
| NotifyPosition(resp); |
| } |
| |
| zx_status_t NelsonAudioStreamOut::ChangeFormat(const audio_proto::StreamSetFmtReq& req) { |
| fifo_depth_ = lib_->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 NelsonAudioStreamOut::ShutdownHook() { lib_->Shutdown(); } |
| |
| zx_status_t NelsonAudioStreamOut::SetGain(const audio_proto::SetGainReq& req) { |
| gain_state_t state; |
| state.gain = req.gain; |
| state.muted = cur_gain_state_.cur_mute; |
| state.agc_enable = cur_gain_state_.cur_agc; |
| auto status = codec_.SetGainState(&state); |
| if (status != ZX_OK) { |
| return status; |
| } |
| cur_gain_state_.cur_gain = state.gain; |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::GetBuffer(const audio_proto::RingBufGetBufferReq& req, |
| uint32_t* out_num_rb_frames, zx::vmo* out_buffer) { |
| size_t size = 0; |
| ring_buffer_vmo_.get_size(&size); |
| uint32_t rb_frames = static_cast<uint32_t>(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; |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::Start(uint64_t* out_start_time) { |
| *out_start_time = lib_->Start(); |
| uint32_t notifs = LoadNotificationsPerRing(); |
| if (notifs) { |
| size_t size = 0; |
| ring_buffer_vmo_.get_size(&size); |
| us_per_notification_ = static_cast<uint32_t>(1000 * size / (frame_size_ * 48 * notifs)); |
| notify_timer_.PostDelayed(dispatcher(), zx::usec(us_per_notification_)); |
| } else { |
| us_per_notification_ = 0; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::Stop() { |
| notify_timer_.Cancel(); |
| us_per_notification_ = 0; |
| lib_->Stop(); |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::AddFormats() { |
| fbl::AllocChecker ac; |
| supported_formats_.reserve(1, &ac); |
| if (!ac.check()) { |
| 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; |
| assert(kWantedFrameRate == 48000); |
| range.min_frames_per_second = kWantedFrameRate; |
| range.max_frames_per_second = kWantedFrameRate; |
| range.flags = ASF_RANGE_FLAG_FPS_48000_FAMILY; |
| |
| supported_formats_.push_back(range); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t NelsonAudioStreamOut::InitBuffer(size_t size) { |
| auto status = zx::vmo::create_contiguous(bti_, size, 0, &ring_buffer_vmo_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s failed to allocate ring buffer vmo - %d", __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", __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; |
| } |
| |
| } // namespace nelson |
| } // namespace audio |
| |
| static zx_status_t nelson_audio_out_bind(void* ctx, zx_device_t* device) { |
| auto stream = audio::SimpleAudioStream::Create<audio::nelson::NelsonAudioStreamOut>(device); |
| if (stream == nullptr) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| return ZX_OK; |
| } |
| |
| static constexpr zx_driver_ops_t nelson_audio_out_driver_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = nelson_audio_out_bind; |
| return ops; |
| }(); |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(nelson_audio_out, nelson_audio_out_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_S905D3), |
| BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_AMLOGIC_TDM), |
| ZIRCON_DRIVER_END(nelson_audio_out) |
| // clang-format on |