blob: dd9a93f670e1dd34fcd451655544d40becd00eba [file] [log] [blame]
// 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 "audio-stream.h"
#include <lib/ddk/debug.h>
#include <lib/ddk/metadata.h>
#include <lib/ddk/platform-defs.h>
#include <lib/simple-codec/simple-codec-helper.h>
#include <lib/zx/clock.h>
#include <math.h>
#include <string.h>
#include <numeric>
#include <optional>
#include <utility>
#include "src/media/audio/drivers/aml-g12-tdm/aml_tdm-bind.h"
namespace audio {
namespace aml_g12 {
AmlG12TdmStream::AmlG12TdmStream(zx_device_t* parent, bool is_input, ddk::PDev pdev,
const ddk::GpioProtocolClient enable_gpio)
: SimpleAudioStream(parent, is_input),
pdev_(std::move(pdev)),
enable_gpio_(std::move(enable_gpio)) {
status_time_ = inspect().GetRoot().CreateInt("status_time", 0);
dma_status_ = inspect().GetRoot().CreateUint("dma_status", 0);
tdm_status_ = inspect().GetRoot().CreateUint("tdm_status", 0);
ring_buffer_physical_address_ = inspect().GetRoot().CreateUint("ring_buffer_physical_address", 0);
}
int AmlG12TdmStream::Thread() {
while (1) {
zx::time timestamp;
irq_.wait(&timestamp);
if (!running_.load()) {
break;
}
zxlogf(ERROR, "DMA status: 0x%08X TDM status: 0x%08X", aml_audio_->GetDmaStatus(),
aml_audio_->GetTdmStatus());
status_time_.Set(timestamp.get());
dma_status_.Set(aml_audio_->GetDmaStatus());
tdm_status_.Set(aml_audio_->GetTdmStatus());
}
zxlogf(INFO, "Exiting interrupt thread");
return 0;
}
void AmlG12TdmStream::InitDaiFormats() {
frame_rate_ =
AmlTdmConfigDevice::kSupportedFrameRates[AmlTdmConfigDevice::kDefaultFrameRateIndex];
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
// Only the PCM signed sample format is supported.
dai_formats_[i].sample_format = SampleFormat::PCM_SIGNED;
dai_formats_[i].frame_rate = frame_rate_;
dai_formats_[i].bits_per_sample = metadata_.dai.bits_per_sample;
dai_formats_[i].bits_per_slot = metadata_.dai.bits_per_slot;
dai_formats_[i].number_of_channels = metadata_.dai.number_of_channels;
dai_formats_[i].channels_to_use_bitmask = metadata_.codecs.channels_to_use_bitmask[i];
switch (metadata_.dai.type) {
case metadata::DaiType::I2s:
dai_formats_[i].frame_format = FrameFormat::I2S;
break;
case metadata::DaiType::StereoLeftJustified:
dai_formats_[i].frame_format = FrameFormat::STEREO_LEFT;
break;
case metadata::DaiType::Tdm1:
dai_formats_[i].frame_format = FrameFormat::TDM1;
break;
default:
ZX_ASSERT(0); // Not supported.
}
}
channels_to_use_ = std::numeric_limits<uint64_t>::max(); // Enable all.
}
zx_status_t AmlG12TdmStream::InitPDev() {
size_t actual = 0;
zx_status_t 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();
if (!pdev_.is_valid()) {
zxlogf(ERROR, "could not get pdev");
return ZX_ERR_NO_RESOURCES;
}
status = pdev_.GetBti(0, &bti_);
if (status != ZX_OK) {
zxlogf(ERROR, "could not obtain bti - %d", status);
return status;
}
ZX_ASSERT(metadata_.codecs.number_of_codecs <= 8);
codecs_.reserve(metadata_.codecs.number_of_codecs);
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
codecs_.push_back(SimpleCodecClient());
char fragment_name[32] = {};
snprintf(fragment_name, 32, "codec-%02lu", i + 1);
status = codecs_[i].SetProtocol(ddk::CodecProtocolClient(parent(), fragment_name));
if (status != ZX_OK) {
zxlogf(ERROR, "could set protocol - %s - %d", fragment_name, status);
return status;
}
}
std::optional<ddk::MmioBuffer> mmio;
status = pdev_.MapMmio(0, &mmio);
if (status != ZX_OK) {
zxlogf(ERROR, "could not get mmio %d", status);
return status;
}
status = pdev_.GetInterrupt(0, 0, &irq_);
if (status != ZX_ERR_OUT_OF_RANGE) { // Not specified in the board file.
if (status != ZX_OK) {
zxlogf(ERROR, "could not get IRQ %d", status);
return status;
}
auto irq_thread = [](void* arg) -> int {
return reinterpret_cast<AmlG12TdmStream*>(arg)->Thread();
};
running_.store(true);
int rc = thrd_create_with_name(&thread_, irq_thread, this, "aml_tdm_irq_thread");
if (rc != thrd_success) {
zxlogf(ERROR, "could not create thread %d", rc);
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;
}
// Initial setup of one page of buffer, just to be safe.
status = InitBuffer(zx_system_get_page_size());
if (status != ZX_OK) {
zxlogf(ERROR, "failed to init buffer %d", status);
return status;
}
status = aml_audio_->SetBuffer(pinned_ring_buffer_.region(0).phys_addr,
pinned_ring_buffer_.region(0).size);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to set buffer %d", status);
return status;
}
status = aml_audio_->InitHW(metadata_, channels_to_use_, frame_rate_);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to init tdm hardware %d", status);
return status;
}
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
auto info = codecs_[i].GetInfo();
if (info.is_error()) {
zxlogf(ERROR, "could get codec info %d", status);
return info.error_value();
}
// Reset and initialize codec after we have configured I2S.
status = codecs_[i].Reset();
if (status != ZX_OK) {
zxlogf(ERROR, "could not reset codec %d", status);
return status;
}
auto supported_formats = codecs_[i].GetDaiFormats();
if (supported_formats.is_error()) {
zxlogf(ERROR, "supported formats error %d", status);
return supported_formats.error_value();
}
if (!IsDaiFormatSupported(dai_formats_[i], supported_formats.value())) {
zxlogf(ERROR, "codec does not support DAI format");
return ZX_ERR_NOT_SUPPORTED;
}
status = codecs_[i].SetDaiFormat(dai_formats_[i]);
if (status != ZX_OK) {
zxlogf(ERROR, "could not set DAI format %d", status);
return status;
}
codecs_[i].Start();
if (status != ZX_OK) {
zxlogf(ERROR, "could not start codec %d", status);
return status;
}
}
zxlogf(INFO, "audio: %s initialized", metadata_.is_input ? "input" : "output");
return ZX_OK;
}
void AmlG12TdmStream::UpdateCodecsGainStateFromCurrent() {
UpdateCodecsGainState({.gain = cur_gain_state_.cur_gain,
.muted = cur_gain_state_.cur_mute,
.agc_enabled = cur_gain_state_.cur_agc});
}
void AmlG12TdmStream::UpdateCodecsGainState(GainState state) {
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
auto state2 = state;
state2.gain += metadata_.codecs.delta_gains[i];
if (override_mute_) {
state2.muted = true;
}
codecs_[i].SetGainState(state2);
}
}
zx_status_t AmlG12TdmStream::InitCodecsGain() {
if (metadata_.codecs.number_of_codecs) {
// Set our gain capabilities.
float min_gain = std::numeric_limits<float>::lowest();
float max_gain = std::numeric_limits<float>::max();
float gain_step = std::numeric_limits<float>::lowest();
bool can_all_mute = true;
bool can_all_agc = true;
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
auto format = codecs_[i].GetGainFormat();
if (format.is_error()) {
zxlogf(ERROR, "Could not get gain format %d", format.error_value());
return format.error_value();
}
min_gain = std::max(min_gain, format->min_gain);
max_gain = std::min(max_gain, format->max_gain);
gain_step = std::max(gain_step, format->gain_step);
can_all_mute = (can_all_mute && format->can_mute);
can_all_agc = (can_all_agc && format->can_agc);
}
// Use first codec as reference initial gain.
auto state = codecs_[0].GetGainState();
if (state.is_error()) {
zxlogf(ERROR, "Could not get gain state %d", state.error_value());
return state.error_value();
}
cur_gain_state_.cur_gain = state->gain;
cur_gain_state_.cur_mute = false;
cur_gain_state_.cur_agc = false;
UpdateCodecsGainState(state.value());
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 = can_all_mute;
cur_gain_state_.can_agc = can_all_agc;
} else {
cur_gain_state_.cur_gain = 0.f;
cur_gain_state_.cur_mute = false;
cur_gain_state_.cur_agc = false;
cur_gain_state_.min_gain = 0.f;
cur_gain_state_.max_gain = 0.f;
cur_gain_state_.gain_step = .0f;
cur_gain_state_.can_mute = false;
cur_gain_state_.can_agc = false;
}
return ZX_OK;
}
zx_status_t AmlG12TdmStream::Init() {
zx_status_t status;
status = InitPDev();
if (status != ZX_OK) {
return status;
}
status = AddFormats();
if (status != ZX_OK) {
return status;
}
status = InitCodecsGain();
if (status != ZX_OK) {
return status;
}
const char* in_out = "out";
if (metadata_.is_input) {
in_out = "in";
}
strncpy(mfr_name_, metadata_.manufacturer, sizeof(mfr_name_));
strncpy(prod_name_, metadata_.product_name, sizeof(prod_name_));
unique_id_ = metadata_.unique_id;
const char* tdm_type = nullptr;
switch (metadata_.dai.type) {
case metadata::DaiType::I2s:
tdm_type = "i2s";
break;
case metadata::DaiType::StereoLeftJustified:
tdm_type = "ljt";
break;
case metadata::DaiType::Tdm1:
tdm_type = "tdm1";
break;
}
snprintf(device_name_, sizeof(device_name_), "%s-audio-%s-%s", prod_name_, tdm_type, in_out);
// 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 AmlG12TdmStream::ProcessRingNotification() {
ScopedToken t(domain_token());
if (us_per_notification_) {
notify_timer_.PostDelayed(dispatcher(), zx::usec(us_per_notification_));
} else {
notify_timer_.Cancel();
return;
}
audio_proto::RingBufPositionNotify resp = {};
resp.hdr.cmd = AUDIO_RB_POSITION_NOTIFY;
resp.monotonic_time = zx::clock::get_monotonic().get();
resp.ring_buffer_pos = aml_audio_->GetRingPosition();
NotifyPosition(resp);
}
zx_status_t AmlG12TdmStream::ChangeFormat(const audio_proto::StreamSetFmtReq& req) {
fifo_depth_ = aml_audio_->fifo_depth();
for (size_t i = 0; i < metadata_.codecs.number_of_external_delays; ++i) {
if (metadata_.codecs.external_delays[i].frequency == req.frames_per_second) {
external_delay_nsec_ = metadata_.codecs.external_delays[i].nsecs;
break;
}
}
if (req.frames_per_second != frame_rate_ || req.channels_to_use_bitmask != channels_to_use_) {
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
// Put codecs in safe state for rate change
auto status = codecs_[i].Stop();
if (status != ZX_OK) {
zxlogf(ERROR, "failed to stop the codec");
return status;
}
}
frame_rate_ = req.frames_per_second;
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
dai_formats_[i].frame_rate = frame_rate_;
}
channels_to_use_ = req.channels_to_use_bitmask;
auto status = aml_audio_->InitHW(metadata_, channels_to_use_, frame_rate_);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to reinitialize the HW");
return status;
}
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
status = codecs_[i].SetDaiFormat(dai_formats_[i]);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to set the DAI format");
return status;
}
// Restart codec
status = codecs_[i].Start();
if (status != ZX_OK) {
zxlogf(ERROR, "failed to restart the codec");
return status;
}
}
}
return ZX_OK;
}
void AmlG12TdmStream::ShutdownHook() {
if (running_.load()) {
running_.store(false);
irq_.destroy();
thrd_join(thread_, NULL);
}
for (size_t i = 0; i < metadata_.codecs.number_of_codecs; ++i) {
// safe the codec so it won't throw clock errors when tdm bus shuts down
codecs_[i].Stop();
}
if (enable_gpio_.is_valid()) {
enable_gpio_.Write(0);
}
aml_audio_->Shutdown();
pinned_ring_buffer_.Unpin();
}
zx_status_t AmlG12TdmStream::SetGain(const audio_proto::SetGainReq& req) {
// Modify parts of the gain state we have received in the request.
if (req.flags & AUDIO_SGF_MUTE_VALID) {
cur_gain_state_.cur_mute = req.flags & AUDIO_SGF_MUTE;
}
if (req.flags & AUDIO_SGF_AGC_VALID) {
cur_gain_state_.cur_agc = req.flags & AUDIO_SGF_AGC;
};
cur_gain_state_.cur_gain = req.gain;
UpdateCodecsGainStateFromCurrent();
return ZX_OK;
}
zx_status_t AmlG12TdmStream::GetBuffer(const audio_proto::RingBufGetBufferReq& req,
uint32_t* out_num_rb_frames, zx::vmo* out_buffer) {
size_t ring_buffer_size =
fbl::round_up<size_t, size_t>(req.min_ring_buffer_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()) {
return ZX_ERR_INVALID_ARGS;
}
size_t vmo_size = fbl::round_up<size_t, size_t>(ring_buffer_size, zx_system_get_page_size());
auto status = InitBuffer(vmo_size);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to init buffer %d", status);
return 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) {
zxlogf(ERROR, "failed to duplicate VMO %d", status);
return status;
}
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);
return status;
}
ring_buffer_physical_address_.Set(pinned_ring_buffer_.region(0).phys_addr);
// This is safe because of the overflow check we made above.
*out_num_rb_frames = static_cast<uint32_t>(out_frames);
return ZX_OK;
}
zx_status_t AmlG12TdmStream::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_ * frame_rate_ / 1000 * notifs));
notify_timer_.PostDelayed(dispatcher(), zx::usec(us_per_notification_));
} else {
us_per_notification_ = 0;
}
override_mute_ = false;
UpdateCodecsGainStateFromCurrent();
return ZX_OK;
}
zx_status_t AmlG12TdmStream::Stop() {
override_mute_ = true;
UpdateCodecsGainStateFromCurrent();
notify_timer_.Cancel();
us_per_notification_ = 0;
aml_audio_->Stop();
return ZX_OK;
}
zx_status_t AmlG12TdmStream::AddFormats() {
fbl::AllocChecker ac;
supported_formats_.reserve(1, &ac);
if (!ac.check()) {
zxlogf(ERROR, "Out of memory, can not create supported formats list");
return ZX_ERR_NO_MEMORY;
}
// Add the range for basic audio support.
audio_stream_format_range_t range = {};
range.min_channels = metadata_.ring_buffer.number_of_channels;
range.max_channels = metadata_.ring_buffer.number_of_channels;
ZX_ASSERT(metadata_.ring_buffer.bytes_per_sample == 2);
range.sample_formats = AUDIO_SAMPLE_FORMAT_16BIT;
for (auto& i : AmlTdmConfigDevice::kSupportedFrameRates) {
range.min_frames_per_second = i;
range.max_frames_per_second = i;
range.flags = ASF_RANGE_FLAG_FPS_CONTINUOUS; // No need to specify family when min == max.
supported_formats_.push_back(range);
}
return ZX_OK;
}
zx_status_t AmlG12TdmStream::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;
}
static zx_status_t audio_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;
}
if (metadata.is_input) {
auto stream = audio::SimpleAudioStream::Create<audio::aml_g12::AmlG12TdmStream>(
device, true, ddk::PDev::FromFragment(device),
ddk::GpioProtocolClient(device, "gpio-enable"));
if (stream == nullptr) {
zxlogf(ERROR, "Could not create aml-g12-tdm driver");
return ZX_ERR_NO_MEMORY;
}
__UNUSED auto dummy = fbl::ExportToRawPtr(&stream);
} else {
auto stream = audio::SimpleAudioStream::Create<audio::aml_g12::AmlG12TdmStream>(
device, false, ddk::PDev::FromFragment(device),
ddk::GpioProtocolClient(device, "gpio-enable"));
if (stream == nullptr) {
zxlogf(ERROR, "Could not create aml-g12-tdm driver");
return ZX_ERR_NO_MEMORY;
}
__UNUSED auto dummy = fbl::ExportToRawPtr(&stream);
}
return ZX_OK;
}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = audio_bind;
return ops;
}();
} // namespace aml_g12
} // namespace audio
// clang-format off
ZIRCON_DRIVER(aml_tdm, audio::aml_g12::driver_ops, "aml-tdm", "0.1");