R4 pre-emptive bringup
Includes stripping out the IC-specific I2S stuff, and doing more manual volume control using pots
This commit is contained in:
+15
-6
@@ -5,18 +5,20 @@
|
||||
*/
|
||||
|
||||
#include "audio_fsm.hpp"
|
||||
#include <memory>
|
||||
#include "audio_decoder.hpp"
|
||||
#include "audio_events.hpp"
|
||||
#include "audio_task.hpp"
|
||||
#include "dac.hpp"
|
||||
#include "fatfs_audio_input.hpp"
|
||||
#include "i2s_audio_output.hpp"
|
||||
#include "i2s_dac.hpp"
|
||||
#include "pipeline.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
drivers::GpioExpander* AudioState::sGpioExpander;
|
||||
std::weak_ptr<drivers::AudioDac> AudioState::sDac;
|
||||
std::shared_ptr<drivers::I2SDac> AudioState::sDac;
|
||||
std::shared_ptr<drivers::DigitalPot> AudioState::sPots;
|
||||
std::weak_ptr<database::Database> AudioState::sDatabase;
|
||||
|
||||
std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
|
||||
@@ -24,14 +26,19 @@ std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
|
||||
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
|
||||
|
||||
auto AudioState::Init(drivers::GpioExpander* gpio_expander,
|
||||
std::weak_ptr<drivers::AudioDac> dac,
|
||||
std::weak_ptr<database::Database> database) -> void {
|
||||
std::weak_ptr<database::Database> database) -> bool {
|
||||
sGpioExpander = gpio_expander;
|
||||
sDac = dac;
|
||||
|
||||
auto dac = drivers::I2SDac::create(gpio_expander);
|
||||
if (!dac) {
|
||||
return false;
|
||||
}
|
||||
sDac.reset(dac.value());
|
||||
sPots.reset(new drivers::DigitalPot(gpio_expander));
|
||||
sDatabase = database;
|
||||
|
||||
sFileSource.reset(new FatfsAudioInput());
|
||||
sI2SOutput.reset(new I2SAudioOutput(sGpioExpander, sDac));
|
||||
sI2SOutput.reset(new I2SAudioOutput(sGpioExpander, sDac, sPots));
|
||||
|
||||
// Perform initial pipeline configuration.
|
||||
// TODO(jacqueline): Factor this out once we have any kind of dynamic
|
||||
@@ -43,6 +50,8 @@ auto AudioState::Init(drivers::GpioExpander* gpio_expander,
|
||||
pipeline->AddInput(sFileSource.get());
|
||||
|
||||
task::StartPipeline(pipeline, sI2SOutput.get());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace states {
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#include "audio_sink.hpp"
|
||||
#include "cbor.h"
|
||||
#include "dac.hpp"
|
||||
#include "esp_err.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
@@ -5,19 +5,21 @@
|
||||
*/
|
||||
|
||||
#include "i2s_audio_output.hpp"
|
||||
#include <stdint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
#include "digital_pot.hpp"
|
||||
#include "esp_err.h"
|
||||
#include "freertos/portmacro.h"
|
||||
|
||||
#include "audio_element.hpp"
|
||||
#include "dac.hpp"
|
||||
#include "freertos/projdefs.h"
|
||||
#include "gpio_expander.hpp"
|
||||
#include "i2s_dac.hpp"
|
||||
#include "result.hpp"
|
||||
#include "stream_info.hpp"
|
||||
|
||||
@@ -26,16 +28,86 @@ static const char* kTag = "I2SOUT";
|
||||
namespace audio {
|
||||
|
||||
I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
|
||||
std::weak_ptr<drivers::AudioDac> dac)
|
||||
: expander_(expander), dac_(dac.lock()), current_config_() {
|
||||
dac_->WriteVolume(127); // for testing
|
||||
std::weak_ptr<drivers::I2SDac> dac,
|
||||
std::weak_ptr<drivers::DigitalPot> pots)
|
||||
: expander_(expander),
|
||||
dac_(dac.lock()),
|
||||
pots_(pots.lock()),
|
||||
current_config_(),
|
||||
left_difference_(0),
|
||||
attenuation_(pots_->GetMaxAttenuation()) {
|
||||
SetVolume(25); // For testing
|
||||
dac_->SetSource(buffer());
|
||||
dac_->Start();
|
||||
}
|
||||
|
||||
I2SAudioOutput::~I2SAudioOutput() {
|
||||
dac_->Stop();
|
||||
dac_->SetSource(nullptr);
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void {
|
||||
int_fast8_t new_difference = balance - left_difference_;
|
||||
left_difference_ = balance;
|
||||
if (attenuation_ + new_difference <= pots_->GetMinAttenuation()) {
|
||||
// Volume is currently very high, so shift the left channel down.
|
||||
pots_->SetRelative(drivers::DigitalPot::Channel::kLeft, -new_difference);
|
||||
} else if (attenuation_ - new_difference >= pots_->GetMaxAttenuation()) {
|
||||
// Volume is currently very low, so shift the left channel up.
|
||||
pots_->SetRelative(drivers::DigitalPot::Channel::kLeft, new_difference);
|
||||
} else {
|
||||
ESP_LOGE(kTag, "volume imbalance higher than attenuation range");
|
||||
}
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::SetVolume(uint_fast8_t percent) -> void {
|
||||
percent = 100 - percent;
|
||||
int_fast8_t target_attenuation =
|
||||
static_cast<int_fast8_t>(static_cast<float>(GetAdjustedMaxAttenuation()) /
|
||||
100.0f * static_cast<float>(percent));
|
||||
target_attenuation -= pots_->GetMinAttenuation();
|
||||
int_fast8_t difference = target_attenuation - attenuation_;
|
||||
pots_->SetRelative(difference);
|
||||
attenuation_ = target_attenuation;
|
||||
ESP_LOGI(kTag, "adjusting attenuation by %idB to %idB", difference,
|
||||
attenuation_);
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::GetVolume() -> uint_fast8_t {
|
||||
// Convert to percentage.
|
||||
uint_fast8_t percent = static_cast<uint_fast8_t>(
|
||||
static_cast<float>(attenuation_) /
|
||||
static_cast<float>(GetAdjustedMaxAttenuation()) * 100.0f);
|
||||
// Invert to get from attenuation to volume.
|
||||
return 100 - percent;
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::GetAdjustedMaxAttenuation() -> int_fast8_t {
|
||||
// Clip to account for imbalance.
|
||||
int_fast8_t adjusted_max =
|
||||
pots_->GetMaxAttenuation() - std::abs(left_difference_);
|
||||
// Shift to be zero minimum.
|
||||
adjusted_max -= pots_->GetMinAttenuation();
|
||||
|
||||
return adjusted_max;
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::AdjustVolumeUp() -> void {
|
||||
if (attenuation_ + left_difference_ <= pots_->GetMinAttenuation()) {
|
||||
return;
|
||||
}
|
||||
attenuation_--;
|
||||
pots_->SetRelative(-1);
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::AdjustVolumeDown() -> void {
|
||||
if (attenuation_ - left_difference_ >= pots_->GetMaxAttenuation()) {
|
||||
return;
|
||||
}
|
||||
attenuation_++;
|
||||
pots_->SetRelative(1);
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
|
||||
if (!std::holds_alternative<StreamInfo::Pcm>(format)) {
|
||||
ESP_LOGI(kTag, "ignoring non-pcm stream (%d)", format.index());
|
||||
@@ -52,29 +124,29 @@ auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
|
||||
ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %lu Hz", pcm.bits_per_sample,
|
||||
pcm.sample_rate);
|
||||
|
||||
drivers::AudioDac::BitsPerSample bps;
|
||||
drivers::I2SDac::BitsPerSample bps;
|
||||
switch (pcm.bits_per_sample) {
|
||||
case 16:
|
||||
bps = drivers::AudioDac::BPS_16;
|
||||
bps = drivers::I2SDac::BPS_16;
|
||||
break;
|
||||
case 24:
|
||||
bps = drivers::AudioDac::BPS_24;
|
||||
bps = drivers::I2SDac::BPS_24;
|
||||
break;
|
||||
case 32:
|
||||
bps = drivers::AudioDac::BPS_32;
|
||||
bps = drivers::I2SDac::BPS_32;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(kTag, "dropping stream with unknown bps");
|
||||
return false;
|
||||
}
|
||||
|
||||
drivers::AudioDac::SampleRate sample_rate;
|
||||
drivers::I2SDac::SampleRate sample_rate;
|
||||
switch (pcm.sample_rate) {
|
||||
case 44100:
|
||||
sample_rate = drivers::AudioDac::SAMPLE_RATE_44_1;
|
||||
sample_rate = drivers::I2SDac::SAMPLE_RATE_44_1;
|
||||
break;
|
||||
case 48000:
|
||||
sample_rate = drivers::AudioDac::SAMPLE_RATE_48;
|
||||
sample_rate = drivers::I2SDac::SAMPLE_RATE_48;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(kTag, "dropping stream with unknown rate");
|
||||
@@ -93,12 +165,4 @@ auto I2SAudioOutput::Send(const cpp::span<std::byte>& data) -> void {
|
||||
dac_->WriteData(data);
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::Log() -> void {
|
||||
dac_->LogStatus();
|
||||
}
|
||||
|
||||
auto I2SAudioOutput::SetVolume(uint8_t volume) -> void {
|
||||
dac_->WriteVolume(volume);
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "audio_events.hpp"
|
||||
#include "dac.hpp"
|
||||
#include "database.hpp"
|
||||
#include "display.hpp"
|
||||
#include "fatfs_audio_input.hpp"
|
||||
#include "gpio_expander.hpp"
|
||||
#include "i2s_audio_output.hpp"
|
||||
#include "i2s_dac.hpp"
|
||||
#include "storage.hpp"
|
||||
#include "tinyfsm.hpp"
|
||||
|
||||
@@ -24,8 +26,7 @@ namespace audio {
|
||||
class AudioState : public tinyfsm::Fsm<AudioState> {
|
||||
public:
|
||||
static auto Init(drivers::GpioExpander* gpio_expander,
|
||||
std::weak_ptr<drivers::AudioDac>,
|
||||
std::weak_ptr<database::Database>) -> void;
|
||||
std::weak_ptr<database::Database>) -> bool;
|
||||
|
||||
virtual ~AudioState() {}
|
||||
|
||||
@@ -41,7 +42,8 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
|
||||
|
||||
protected:
|
||||
static drivers::GpioExpander* sGpioExpander;
|
||||
static std::weak_ptr<drivers::AudioDac> sDac;
|
||||
static std::shared_ptr<drivers::I2SDac> sDac;
|
||||
static std::shared_ptr<drivers::DigitalPot> sPots;
|
||||
static std::weak_ptr<database::Database> sDatabase;
|
||||
|
||||
static std::unique_ptr<FatfsAudioInput> sFileSource;
|
||||
|
||||
@@ -40,9 +40,14 @@ class IAudioSink {
|
||||
free(metadata_);
|
||||
}
|
||||
|
||||
virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0;
|
||||
virtual auto SetVolume(uint_fast8_t percent) -> void = 0;
|
||||
virtual auto GetVolume() -> uint_fast8_t = 0;
|
||||
virtual auto AdjustVolumeUp() -> void = 0;
|
||||
virtual auto AdjustVolumeDown() -> void = 0;
|
||||
|
||||
virtual auto Configure(const StreamInfo::Format& format) -> bool = 0;
|
||||
virtual auto Send(const cpp::span<std::byte>& data) -> void = 0;
|
||||
virtual auto Log() -> void {}
|
||||
|
||||
auto buffer() -> StreamBufferHandle_t { return handle_; }
|
||||
};
|
||||
|
||||
@@ -6,16 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "audio_element.hpp"
|
||||
#include "audio_sink.hpp"
|
||||
#include "dac.hpp"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "pipeline.hpp"
|
||||
#include "stream_buffer.hpp"
|
||||
|
||||
namespace audio {
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
#include "chunk.hpp"
|
||||
#include "result.hpp"
|
||||
|
||||
#include "dac.hpp"
|
||||
#include "digital_pot.hpp"
|
||||
#include "gpio_expander.hpp"
|
||||
#include "i2s_dac.hpp"
|
||||
#include "stream_info.hpp"
|
||||
|
||||
namespace audio {
|
||||
@@ -24,23 +25,32 @@ namespace audio {
|
||||
class I2SAudioOutput : public IAudioSink {
|
||||
public:
|
||||
I2SAudioOutput(drivers::GpioExpander* expander,
|
||||
std::weak_ptr<drivers::AudioDac> dac);
|
||||
std::weak_ptr<drivers::I2SDac> dac,
|
||||
std::weak_ptr<drivers::DigitalPot> pots);
|
||||
~I2SAudioOutput();
|
||||
|
||||
auto SetVolumeImbalance(int_fast8_t balance) -> void override;
|
||||
auto SetVolume(uint_fast8_t percent) -> void override;
|
||||
auto GetVolume() -> uint_fast8_t override;
|
||||
auto AdjustVolumeUp() -> void override;
|
||||
auto AdjustVolumeDown() -> void override;
|
||||
|
||||
auto Configure(const StreamInfo::Format& format) -> bool override;
|
||||
auto Send(const cpp::span<std::byte>& data) -> void override;
|
||||
auto Log() -> void override;
|
||||
|
||||
I2SAudioOutput(const I2SAudioOutput&) = delete;
|
||||
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;
|
||||
|
||||
private:
|
||||
auto SetVolume(uint8_t volume) -> void;
|
||||
auto GetAdjustedMaxAttenuation() -> int_fast8_t;
|
||||
|
||||
drivers::GpioExpander* expander_;
|
||||
std::shared_ptr<drivers::AudioDac> dac_;
|
||||
std::shared_ptr<drivers::I2SDac> dac_;
|
||||
std::shared_ptr<drivers::DigitalPot> pots_;
|
||||
|
||||
std::optional<StreamInfo::Pcm> current_config_;
|
||||
int_fast8_t left_difference_;
|
||||
uint_fast8_t attenuation_;
|
||||
};
|
||||
|
||||
} // namespace audio
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
idf_component_register(
|
||||
SRCS "touchwheel.cpp" "dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
|
||||
"spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp"
|
||||
SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpio_expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
|
||||
"spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "digital_pot.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks")
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#include "dac.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "assert.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/i2s_common.h"
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_attr.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/projdefs.h"
|
||||
#include "hal/gpio_types.h"
|
||||
#include "hal/i2c_types.h"
|
||||
|
||||
#include "gpio_expander.hpp"
|
||||
#include "hal/i2s_types.h"
|
||||
#include "i2c.hpp"
|
||||
#include "soc/clk_tree_defs.h"
|
||||
#include "sys/_stdint.h"
|
||||
|
||||
namespace drivers {
|
||||
|
||||
static const char* kTag = "AUDIODAC";
|
||||
static const uint8_t kPcm5122Address = 0x4C;
|
||||
static const i2s_port_t kI2SPort = I2S_NUM_0;
|
||||
|
||||
auto AudioDac::create(GpioExpander* expander) -> cpp::result<AudioDac*, Error> {
|
||||
// TODO: tune.
|
||||
i2s_chan_handle_t i2s_handle;
|
||||
i2s_chan_config_t channel_config =
|
||||
I2S_CHANNEL_DEFAULT_CONFIG(kI2SPort, I2S_ROLE_MASTER);
|
||||
// Use the maximum possible DMA buffer size, since a smaller number of large
|
||||
// copies is faster than a large number of small copies.
|
||||
channel_config.dma_frame_num = 1024;
|
||||
// Triple buffering should be enough to keep samples flowing smoothly.
|
||||
// TODO(jacqueline): verify this with 192kHz 32bps.
|
||||
channel_config.dma_desc_num = 4;
|
||||
// channel_config.auto_clear = true;
|
||||
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &i2s_handle, NULL));
|
||||
//
|
||||
// First, instantiate the instance so it can do all of its power on
|
||||
// configuration.
|
||||
std::unique_ptr<AudioDac> dac =
|
||||
std::make_unique<AudioDac>(expander, i2s_handle);
|
||||
|
||||
// Whilst we wait for the initial boot, we can work on installing the I2S
|
||||
// driver.
|
||||
i2s_std_config_t i2s_config = {
|
||||
.clk_cfg = dac->clock_config_,
|
||||
.slot_cfg = dac->slot_config_,
|
||||
.gpio_cfg = {.mclk = GPIO_NUM_0,
|
||||
.bclk = GPIO_NUM_26,
|
||||
.ws = GPIO_NUM_27,
|
||||
.dout = GPIO_NUM_5,
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags =
|
||||
{
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = true,
|
||||
}},
|
||||
};
|
||||
|
||||
// gpio_set_direction(GPIO_NUM_0, GPIO_MODE_OUTPUT);
|
||||
// gpio_set_level(GPIO_NUM_0, 0);
|
||||
|
||||
if (esp_err_t err =
|
||||
i2s_channel_init_std_mode(i2s_handle, &i2s_config) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "failed to initialise i2s channel %x", err);
|
||||
return cpp::fail(Error::FAILED_TO_INSTALL_I2S);
|
||||
}
|
||||
|
||||
// Make sure the DAC has booted before sending commands to it.
|
||||
bool is_booted = dac->WaitForPowerState(
|
||||
[](bool booted, PowerState state) { return booted; });
|
||||
if (!is_booted) {
|
||||
ESP_LOGE(kTag, "Timed out waiting for boot");
|
||||
return cpp::fail(Error::FAILED_TO_BOOT);
|
||||
}
|
||||
|
||||
// The DAC should be booted but in power down mode, but it might not be if we
|
||||
// didn't shut down cleanly. Reset it to ensure it is in a consistent state.
|
||||
dac->WriteRegister(pcm512x::POWER, 1 << 4);
|
||||
dac->WriteRegister(pcm512x::RESET, 0b10001);
|
||||
|
||||
// Use BCK for the internal PLL.
|
||||
// dac->WriteRegister(Register::PLL_CLOCK_SOURCE, 1 << 4);
|
||||
// dac->WriteRegister(Register::DAC_CLOCK_SOURCE, 0b11 << 5);
|
||||
|
||||
// dac->WriteRegister(Register::PLL_ENABLE, 0);
|
||||
// dac->WriteRegister(Register::DAC_CLOCK_SOURCE, 0b0110000);
|
||||
// dac->WriteRegister(Register::CLOCK_ERRORS, 0b01000001);
|
||||
// dac->WriteRegister(Register::I2S_FORMAT, 0b110000);
|
||||
// dac->WriteRegister(Register::INTERPOLATION, 1 << 4);
|
||||
|
||||
dac->Reconfigure(BPS_16, SAMPLE_RATE_44_1);
|
||||
|
||||
// Now configure the DAC for standard auto-clock SCK mode.
|
||||
|
||||
// Enable auto clocking, and do your best to carry on despite errors.
|
||||
// dac->WriteRegister(Register::CLOCK_ERRORS, 0b1111101);
|
||||
|
||||
// i2s_channel_enable(dac->i2s_handle_);
|
||||
|
||||
dac->WaitForPowerState([](bool booted, PowerState state) {
|
||||
return state == RUN || state == STANDBY;
|
||||
});
|
||||
|
||||
return dac.release();
|
||||
}
|
||||
|
||||
AudioDac::AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle)
|
||||
: gpio_(gpio),
|
||||
i2s_handle_(i2s_handle),
|
||||
i2s_active_(false),
|
||||
active_page_(),
|
||||
clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)),
|
||||
slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
|
||||
I2S_SLOT_MODE_STEREO)) {
|
||||
clock_config_.clk_src = I2S_CLK_SRC_PLL_160M;
|
||||
gpio_->set_pin(GpioExpander::AMP_EN, true);
|
||||
gpio_->Write();
|
||||
}
|
||||
|
||||
AudioDac::~AudioDac() {
|
||||
i2s_channel_disable(i2s_handle_);
|
||||
i2s_del_channel(i2s_handle_);
|
||||
gpio_->set_pin(GpioExpander::AMP_EN, false);
|
||||
gpio_->Write();
|
||||
}
|
||||
|
||||
void AudioDac::WriteVolume(uint8_t volume) {
|
||||
// Left channel.
|
||||
WriteRegister(pcm512x::DIGITAL_VOLUME_2, volume);
|
||||
// Right channel.
|
||||
WriteRegister(pcm512x::DIGITAL_VOLUME_3, volume);
|
||||
}
|
||||
|
||||
std::pair<bool, AudioDac::PowerState> AudioDac::ReadPowerState() {
|
||||
uint8_t result = ReadRegister(pcm512x::POWER_STATE);
|
||||
bool is_booted = result >> 7;
|
||||
PowerState detail = (PowerState)(result & 0b1111);
|
||||
return std::pair(is_booted, detail);
|
||||
}
|
||||
|
||||
bool AudioDac::WaitForPowerState(
|
||||
std::function<bool(bool, AudioDac::PowerState)> predicate) {
|
||||
bool has_matched = false;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::pair<bool, PowerState> result = ReadPowerState();
|
||||
has_matched = predicate(result.first, result.second);
|
||||
if (has_matched) {
|
||||
break;
|
||||
} else {
|
||||
ESP_LOGI(kTag, "Waiting for power state (was %d 0x%x)", result.first,
|
||||
(uint8_t)result.second);
|
||||
vTaskDelay(pdMS_TO_TICKS(250));
|
||||
}
|
||||
}
|
||||
return has_matched;
|
||||
}
|
||||
|
||||
auto AudioDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
|
||||
if (i2s_active_) {
|
||||
WriteRegister(pcm512x::MUTE, 0b10001);
|
||||
vTaskDelay(1);
|
||||
WriteRegister(pcm512x::POWER, 1 << 4);
|
||||
i2s_channel_disable(i2s_handle_);
|
||||
}
|
||||
|
||||
// I2S reconfiguration.
|
||||
uint8_t bps_bits = 0;
|
||||
switch (bps) {
|
||||
case BPS_16:
|
||||
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
|
||||
bps_bits = 0;
|
||||
break;
|
||||
case BPS_24:
|
||||
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_24BIT;
|
||||
bps_bits = 0b10;
|
||||
break;
|
||||
case BPS_32:
|
||||
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT;
|
||||
bps_bits = 0b11;
|
||||
break;
|
||||
}
|
||||
ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(i2s_handle_, &slot_config_));
|
||||
|
||||
clock_config_.sample_rate_hz = rate;
|
||||
// If we have an MCLK/SCK, then it must be a multiple of both the sample rate
|
||||
// and the bit clock. At 24 BPS, we therefore have to change the MCLK multiple
|
||||
// to avoid issues at some sample rates. (e.g. 48KHz)
|
||||
clock_config_.mclk_multiple =
|
||||
bps == BPS_24 ? I2S_MCLK_MULTIPLE_384 : I2S_MCLK_MULTIPLE_256;
|
||||
ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(i2s_handle_, &clock_config_));
|
||||
|
||||
// DAC reconfiguration.
|
||||
// Inspired heavily by https://github.com/tommag/PCM51xx_Arduino (MIT).
|
||||
|
||||
// Check that the bit clock (PLL input) is between 1MHz and 50MHz. It always
|
||||
// should be.
|
||||
uint32_t bckFreq = rate * bps * 2;
|
||||
if (bckFreq < 1000000 || bckFreq > 50000000) {
|
||||
ESP_LOGE(kTag, "bck freq out of range");
|
||||
return;
|
||||
}
|
||||
|
||||
// 24 bits is not supported for 44.1kHz and 48kHz.
|
||||
if ((rate == SAMPLE_RATE_44_1 || rate == SAMPLE_RATE_48) && bps == BPS_24) {
|
||||
// TODO(jacqueline): I think this *can* be implemented, but requires a bunch
|
||||
// of maths.
|
||||
ESP_LOGE(kTag, "sample rate and bps mismatch");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize system clock from the I2S BCK input
|
||||
// Disable clock autoset and ignore SCK detection
|
||||
WriteRegister(pcm512x::ERROR_DETECT, 0x1A);
|
||||
// Set PLL clock source to BCK
|
||||
WriteRegister(pcm512x::PLL_REF, 0x10);
|
||||
// Set DAC clock source to PLL output
|
||||
WriteRegister(pcm512x::DAC_REF, 0x10);
|
||||
|
||||
// PLL configuration
|
||||
int p, j, d, r;
|
||||
|
||||
// Clock dividers
|
||||
int nmac, ndac, ncp, dosr, idac;
|
||||
|
||||
if (rate == SAMPLE_RATE_11_025 || rate == SAMPLE_RATE_22_05 ||
|
||||
rate == SAMPLE_RATE_44_1) {
|
||||
// 44.1kHz and derivatives.
|
||||
// P = 1, R = 2, D = 0 for all supported combinations.
|
||||
// Set J to have PLL clk = 90.3168 MHz
|
||||
p = 1;
|
||||
r = 2;
|
||||
j = 90316800 / bckFreq / r;
|
||||
d = 0;
|
||||
|
||||
// Derive clocks from the 90.3168MHz PLL
|
||||
nmac = 2;
|
||||
ndac = 16;
|
||||
ncp = 4;
|
||||
dosr = 8;
|
||||
idac = 1024; // DSP clock / sample rate
|
||||
} else {
|
||||
// 8kHz and multiples.
|
||||
// PLL config for a 98.304 MHz PLL clk
|
||||
if (bps == BPS_24 && bckFreq > 1536000) {
|
||||
p = 3;
|
||||
} else if (bckFreq > 12288000) {
|
||||
p = 2;
|
||||
} else {
|
||||
p = 1;
|
||||
}
|
||||
|
||||
r = 2;
|
||||
j = 98304000 / (bckFreq / p) / r;
|
||||
d = 0;
|
||||
|
||||
// Derive clocks from the 98.304MHz PLL
|
||||
switch (rate) {
|
||||
case SAMPLE_RATE_16:
|
||||
nmac = 6;
|
||||
break;
|
||||
case SAMPLE_RATE_32:
|
||||
nmac = 3;
|
||||
break;
|
||||
default:
|
||||
nmac = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
ndac = 16;
|
||||
ncp = 4;
|
||||
dosr = 384000 / rate;
|
||||
idac = 98304000 / nmac / rate; // DSP clock / sample rate
|
||||
}
|
||||
|
||||
// Configure PLL
|
||||
WriteRegister(pcm512x::PLL_COEFF_0, p - 1);
|
||||
WriteRegister(pcm512x::PLL_COEFF_1, j);
|
||||
WriteRegister(pcm512x::PLL_COEFF_2, (d >> 8) & 0x3F);
|
||||
WriteRegister(pcm512x::PLL_COEFF_3, d & 0xFF);
|
||||
WriteRegister(pcm512x::PLL_COEFF_4, r - 1);
|
||||
|
||||
// Clock dividers
|
||||
WriteRegister(pcm512x::DSP_CLKDIV, nmac - 1);
|
||||
WriteRegister(pcm512x::DAC_CLKDIV, ndac - 1);
|
||||
WriteRegister(pcm512x::NCP_CLKDIV, ncp - 1);
|
||||
WriteRegister(pcm512x::OSR_CLKDIV, dosr - 1);
|
||||
|
||||
// IDAC (nb of DSP clock cycles per sample)
|
||||
WriteRegister(pcm512x::IDAC_1, (idac >> 8) & 0xFF);
|
||||
WriteRegister(pcm512x::IDAC_2, idac & 0xFF);
|
||||
|
||||
// FS speed mode
|
||||
int speedMode;
|
||||
if (rate <= SAMPLE_RATE_48) {
|
||||
speedMode = 0;
|
||||
} else if (rate <= SAMPLE_RATE_96) {
|
||||
speedMode = 1;
|
||||
} else if (rate <= SAMPLE_RATE_192) {
|
||||
speedMode = 2;
|
||||
} else {
|
||||
speedMode = 3;
|
||||
}
|
||||
WriteRegister(pcm512x::FS_SPEED_MODE, speedMode);
|
||||
|
||||
WriteRegister(pcm512x::I2S_1, (0b11 << 4) | bps_bits);
|
||||
WriteRegister(pcm512x::I2S_2, 0);
|
||||
|
||||
// Configuration is all done, so we can now bring the DAC and I2S stream back
|
||||
// up. I2S first, since otherwise the DAC will see that there's no clocks and
|
||||
// shut itself down.
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_));
|
||||
WriteRegister(pcm512x::POWER, 0);
|
||||
|
||||
if (i2s_active_) {
|
||||
vTaskDelay(1);
|
||||
WriteRegister(pcm512x::MUTE, 0);
|
||||
}
|
||||
i2s_active_ = true;
|
||||
}
|
||||
|
||||
auto AudioDac::WriteData(const cpp::span<const std::byte>& data) -> void {
|
||||
std::size_t bytes_written = 0;
|
||||
esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(),
|
||||
&bytes_written, portMAX_DELAY);
|
||||
if (err != ESP_ERR_TIMEOUT) {
|
||||
ESP_ERROR_CHECK(err);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle,
|
||||
i2s_event_data_t* event,
|
||||
void* user_ctx) -> bool {
|
||||
if (event == nullptr || user_ctx == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (event->data == nullptr || event->size == 0) {
|
||||
return false;
|
||||
}
|
||||
uint8_t** buf = reinterpret_cast<uint8_t**>(event->data);
|
||||
StreamBufferHandle_t src = reinterpret_cast<StreamBufferHandle_t>(user_ctx);
|
||||
BaseType_t ret = false;
|
||||
std::size_t bytes_received =
|
||||
xStreamBufferReceiveFromISR(src, *buf, event->size, &ret);
|
||||
if (bytes_received < event->size) {
|
||||
memset(*buf + bytes_received, 0, event->size - bytes_received);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto AudioDac::SetSource(StreamBufferHandle_t buffer) -> void {
|
||||
if (i2s_active_) {
|
||||
ESP_ERROR_CHECK(i2s_channel_disable(i2s_handle_));
|
||||
}
|
||||
i2s_event_callbacks_t callbacks{
|
||||
.on_recv = NULL,
|
||||
.on_recv_q_ovf = NULL,
|
||||
.on_sent = NULL,
|
||||
.on_send_q_ovf = NULL,
|
||||
};
|
||||
if (buffer != nullptr) {
|
||||
callbacks.on_sent = &callback;
|
||||
}
|
||||
i2s_channel_register_event_callback(i2s_handle_, &callbacks, buffer);
|
||||
if (i2s_active_) {
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_));
|
||||
}
|
||||
}
|
||||
|
||||
auto AudioDac::Stop() -> void {
|
||||
LogStatus();
|
||||
WriteRegister(pcm512x::POWER, 1 << 4);
|
||||
i2s_channel_disable(i2s_handle_);
|
||||
}
|
||||
|
||||
#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c"
|
||||
#define BYTE_TO_BINARY(byte) \
|
||||
(byte & 0x80 ? '1' : '0'), (byte & 0x40 ? '1' : '0'), \
|
||||
(byte & 0x20 ? '1' : '0'), (byte & 0x10 ? '1' : '0'), \
|
||||
(byte & 0x08 ? '1' : '0'), (byte & 0x04 ? '1' : '0'), \
|
||||
(byte & 0x02 ? '1' : '0'), (byte & 0x01 ? '1' : '0')
|
||||
|
||||
auto AudioDac::LogStatus() -> void {
|
||||
uint8_t res;
|
||||
|
||||
res = ReadRegister(pcm512x::RATE_DET_1);
|
||||
ESP_LOGI(kTag, "detected sample rate (want 3): %u", (res & 0b01110000) >> 4);
|
||||
ESP_LOGI(kTag, "detected SCK ratio (want 6): %u", res && 0b1111);
|
||||
|
||||
res = ReadRegister(pcm512x::RATE_DET_3);
|
||||
ESP_LOGI(kTag, "detected BCK (want... 16? 32?): %u", res);
|
||||
|
||||
res = ReadRegister(pcm512x::RATE_DET_4);
|
||||
ESP_LOGI(kTag, "clock errors (want zeroes): ");
|
||||
ESP_LOGI(kTag, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b1111111));
|
||||
|
||||
res = ReadRegister(pcm512x::CLOCK_STATUS);
|
||||
ESP_LOGI(kTag, "clock status (want zeroes): ");
|
||||
ESP_LOGI(kTag, BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(res & 0b10111));
|
||||
|
||||
res = ReadRegister(pcm512x::DIGITAL_MUTE_DET);
|
||||
ESP_LOGI(kTag, "automute status (want 0): %u", res & 0b10001);
|
||||
|
||||
auto power = ReadPowerState();
|
||||
ESP_LOGI(kTag, "current power state (want 5): %u", power.second);
|
||||
}
|
||||
|
||||
void AudioDac::WriteRegister(pcm512x::Register r, uint8_t val) {
|
||||
SelectPage(r.page);
|
||||
WriteRegisterRaw(r.reg, val);
|
||||
}
|
||||
|
||||
uint8_t AudioDac::ReadRegister(pcm512x::Register r) {
|
||||
SelectPage(r.page);
|
||||
return ReadRegisterRaw(r.reg);
|
||||
}
|
||||
|
||||
void AudioDac::SelectPage(uint8_t page) {
|
||||
if (active_page_ && active_page_ == page) {
|
||||
return;
|
||||
}
|
||||
WriteRegisterRaw(0, page);
|
||||
active_page_ = page;
|
||||
}
|
||||
|
||||
void AudioDac::WriteRegisterRaw(uint8_t reg, uint8_t val) {
|
||||
I2CTransaction transaction;
|
||||
transaction.start()
|
||||
.write_addr(kPcm5122Address, I2C_MASTER_WRITE)
|
||||
.write_ack(reg, val)
|
||||
.stop();
|
||||
// TODO: Retry once?
|
||||
transaction.Execute();
|
||||
}
|
||||
|
||||
uint8_t AudioDac::ReadRegisterRaw(uint8_t reg) {
|
||||
uint8_t result = 0;
|
||||
I2CTransaction transaction;
|
||||
transaction.start()
|
||||
.write_addr(kPcm5122Address, I2C_MASTER_WRITE)
|
||||
.write_ack(reg)
|
||||
.start()
|
||||
.write_addr(kPcm5122Address, I2C_MASTER_READ)
|
||||
.read(&result, I2C_MASTER_NACK)
|
||||
.stop();
|
||||
|
||||
transaction.Execute();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace drivers
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#include "digital_pot.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace drivers {
|
||||
|
||||
using GpioExpander::VOL_LEFT;
|
||||
using GpioExpander::VOL_RIGHT;
|
||||
using GpioExpander::VOL_UP_DOWN;
|
||||
using GpioExpander::VOL_Z_CROSS;
|
||||
|
||||
DigitalPot::DigitalPot(GpioExpander* gpios) : gpios_(gpios) {
|
||||
gpios_->set_pin(VOL_Z_CROSS, true); // Active-low
|
||||
gpios_->set_pin(VOL_UP_DOWN, true);
|
||||
gpios_->set_pin(VOL_LEFT, false);
|
||||
gpios_->set_pin(VOL_RIGHT, false);
|
||||
gpios_->Write();
|
||||
|
||||
// Power-on reset sets attenuation to maximum anyway, but we want to be safe
|
||||
// and not blow anyone's ears out.
|
||||
for (int i = 0; i < 32; i++) {
|
||||
gpios_->set_pin(VOL_LEFT, true);
|
||||
gpios_->set_pin(VOL_RIGHT, true);
|
||||
gpios_->Write();
|
||||
gpios_->set_pin(VOL_LEFT, false);
|
||||
gpios_->set_pin(VOL_RIGHT, false);
|
||||
gpios_->Write();
|
||||
}
|
||||
}
|
||||
|
||||
auto DigitalPot::SetRelative(int_fast8_t change) -> void {
|
||||
if (change == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpios_->set_pin(VOL_UP_DOWN, change > 0);
|
||||
gpios_->Write();
|
||||
|
||||
for (int i = 0; i < std::abs(change); i++) {
|
||||
gpios_->set_pin(VOL_LEFT, true);
|
||||
gpios_->set_pin(VOL_RIGHT, true);
|
||||
gpios_->Write();
|
||||
gpios_->set_pin(VOL_LEFT, false);
|
||||
gpios_->set_pin(VOL_RIGHT, false);
|
||||
gpios_->Write();
|
||||
}
|
||||
}
|
||||
|
||||
auto DigitalPot::SetRelative(Channel ch, int_fast8_t change) -> void {
|
||||
if (change == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
GpioExpander::Pin pin = (ch == Channel::kLeft) ? VOL_LEFT : VOL_RIGHT;
|
||||
gpios_->set_pin(VOL_UP_DOWN, change > 0);
|
||||
gpios_->Write();
|
||||
|
||||
for (int i = 0; i < std::abs(change); i++) {
|
||||
gpios_->set_pin(pin, true);
|
||||
gpios_->Write();
|
||||
gpios_->set_pin(pin, false);
|
||||
gpios_->Write();
|
||||
}
|
||||
}
|
||||
|
||||
auto DigitalPot::SetZeroCrossDetect(bool enabled) -> void {
|
||||
gpios_->set_pin(VOL_Z_CROSS, !enabled); // Active-low
|
||||
gpios_->Write();
|
||||
}
|
||||
|
||||
auto DigitalPot::GetMaxAttenuation() -> int_fast8_t {
|
||||
return 31;
|
||||
}
|
||||
|
||||
auto DigitalPot::GetMinAttenuation() -> int_fast8_t {
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace drivers
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#include "i2s_dac.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "assert.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/i2s_common.h"
|
||||
#include "driver/i2s_std.h"
|
||||
#include "esp_attr.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/projdefs.h"
|
||||
#include "hal/gpio_types.h"
|
||||
#include "hal/i2c_types.h"
|
||||
|
||||
#include "gpio_expander.hpp"
|
||||
#include "hal/i2s_types.h"
|
||||
#include "i2c.hpp"
|
||||
#include "soc/clk_tree_defs.h"
|
||||
#include "sys/_stdint.h"
|
||||
|
||||
namespace drivers {
|
||||
|
||||
static const char* kTag = "i2s_dac";
|
||||
static const i2s_port_t kI2SPort = I2S_NUM_0;
|
||||
|
||||
auto I2SDac::create(GpioExpander* expander) -> std::optional<I2SDac*> {
|
||||
i2s_chan_handle_t i2s_handle;
|
||||
i2s_chan_config_t channel_config =
|
||||
I2S_CHANNEL_DEFAULT_CONFIG(kI2SPort, I2S_ROLE_MASTER);
|
||||
|
||||
// Use the maximum possible DMA buffer size, since a smaller number of large
|
||||
// copies is faster than a large number of small copies.
|
||||
channel_config.dma_frame_num = 1024;
|
||||
// Triple buffering should be enough to keep samples flowing smoothly.
|
||||
// TODO(jacqueline): verify this with 192kHz 32bps.
|
||||
channel_config.dma_desc_num = 4;
|
||||
// channel_config.auto_clear = true;
|
||||
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &i2s_handle, NULL));
|
||||
//
|
||||
// First, instantiate the instance so it can do all of its power on
|
||||
// configuration.
|
||||
std::unique_ptr<I2SDac> dac = std::make_unique<I2SDac>(expander, i2s_handle);
|
||||
|
||||
// Whilst we wait for the initial boot, we can work on installing the I2S
|
||||
// driver.
|
||||
i2s_std_config_t i2s_config = {
|
||||
.clk_cfg = dac->clock_config_,
|
||||
.slot_cfg = dac->slot_config_,
|
||||
.gpio_cfg = {.mclk = GPIO_NUM_0,
|
||||
.bclk = GPIO_NUM_26,
|
||||
.ws = GPIO_NUM_27,
|
||||
.dout = GPIO_NUM_5,
|
||||
.din = I2S_GPIO_UNUSED,
|
||||
.invert_flags =
|
||||
{
|
||||
.mclk_inv = false,
|
||||
.bclk_inv = false,
|
||||
.ws_inv = true,
|
||||
}},
|
||||
};
|
||||
|
||||
if (esp_err_t err =
|
||||
i2s_channel_init_std_mode(i2s_handle, &i2s_config) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "failed to initialise i2s channel %x", err);
|
||||
return {};
|
||||
}
|
||||
|
||||
return dac.release();
|
||||
}
|
||||
|
||||
I2SDac::I2SDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle)
|
||||
: gpio_(gpio),
|
||||
i2s_handle_(i2s_handle),
|
||||
i2s_active_(false),
|
||||
active_page_(),
|
||||
clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(44100)),
|
||||
slot_config_(I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
|
||||
I2S_SLOT_MODE_STEREO)) {
|
||||
clock_config_.clk_src = I2S_CLK_SRC_PLL_160M;
|
||||
gpio_->set_pin(GpioExpander::AMP_EN, false);
|
||||
gpio_->Write();
|
||||
}
|
||||
|
||||
I2SDac::~I2SDac() {
|
||||
Stop();
|
||||
i2s_del_channel(i2s_handle_);
|
||||
}
|
||||
|
||||
auto I2SDac::Start() -> void {
|
||||
gpio_->set_pin(GpioExpander::AMP_EN, true);
|
||||
gpio_->Write();
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
i2s_channel_enable(i2s_handle_);
|
||||
|
||||
i2s_active_ = true;
|
||||
}
|
||||
|
||||
auto I2SDac::Stop() -> void {
|
||||
i2s_channel_disable(i2s_handle_);
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
|
||||
gpio_->set_pin(GpioExpander::AMP_EN, false);
|
||||
gpio_->Write();
|
||||
|
||||
i2s_active_ = false;
|
||||
}
|
||||
|
||||
auto I2SDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
|
||||
if (i2s_active_) {
|
||||
i2s_channel_disable(i2s_handle_);
|
||||
}
|
||||
|
||||
uint8_t bps_bits = 0;
|
||||
switch (bps) {
|
||||
case BPS_16:
|
||||
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
|
||||
bps_bits = 0;
|
||||
break;
|
||||
case BPS_24:
|
||||
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_24BIT;
|
||||
bps_bits = 0b10;
|
||||
break;
|
||||
case BPS_32:
|
||||
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT;
|
||||
bps_bits = 0b11;
|
||||
break;
|
||||
}
|
||||
ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(i2s_handle_, &slot_config_));
|
||||
|
||||
clock_config_.sample_rate_hz = rate;
|
||||
// If we have an MCLK/SCK, then it must be a multiple of both the sample rate
|
||||
// and the bit clock. At 24 BPS, we therefore have to change the MCLK multiple
|
||||
// to avoid issues at some sample rates. (e.g. 48KHz)
|
||||
clock_config_.mclk_multiple =
|
||||
bps == BPS_24 ? I2S_MCLK_MULTIPLE_384 : I2S_MCLK_MULTIPLE_256;
|
||||
ESP_ERROR_CHECK(i2s_channel_reconfig_std_clock(i2s_handle_, &clock_config_));
|
||||
|
||||
if (i2s_active_) {
|
||||
i2s_channel_enable(i2s_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
auto I2SDac::WriteData(const cpp::span<const std::byte>& data) -> void {
|
||||
std::size_t bytes_written = 0;
|
||||
esp_err_t err = i2s_channel_write(i2s_handle_, data.data(), data.size_bytes(),
|
||||
&bytes_written, portMAX_DELAY);
|
||||
if (err != ESP_ERR_TIMEOUT) {
|
||||
ESP_ERROR_CHECK(err);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle,
|
||||
i2s_event_data_t* event,
|
||||
void* user_ctx) -> bool {
|
||||
if (event == nullptr || user_ctx == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (event->data == nullptr || event->size == 0) {
|
||||
return false;
|
||||
}
|
||||
uint8_t** buf = reinterpret_cast<uint8_t**>(event->data);
|
||||
StreamBufferHandle_t src = reinterpret_cast<StreamBufferHandle_t>(user_ctx);
|
||||
BaseType_t ret = false;
|
||||
std::size_t bytes_received =
|
||||
xStreamBufferReceiveFromISR(src, *buf, event->size, &ret);
|
||||
if (bytes_received < event->size) {
|
||||
memset(*buf + bytes_received, 0, event->size - bytes_received);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto I2SDac::SetSource(StreamBufferHandle_t buffer) -> void {
|
||||
if (i2s_active_) {
|
||||
ESP_ERROR_CHECK(i2s_channel_disable(i2s_handle_));
|
||||
}
|
||||
i2s_event_callbacks_t callbacks{
|
||||
.on_recv = NULL,
|
||||
.on_recv_q_ovf = NULL,
|
||||
.on_sent = NULL,
|
||||
.on_send_q_ovf = NULL,
|
||||
};
|
||||
if (buffer != nullptr) {
|
||||
callbacks.on_sent = &callback;
|
||||
}
|
||||
i2s_channel_register_event_callback(i2s_handle_, &callbacks, buffer);
|
||||
if (i2s_active_) {
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(i2s_handle_));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace drivers
|
||||
@@ -1,204 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "driver/i2s_std.h"
|
||||
#include "driver/i2s_types.h"
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/stream_buffer.h"
|
||||
#include "result.hpp"
|
||||
#include "span.hpp"
|
||||
|
||||
#include "gpio_expander.hpp"
|
||||
#include "sys/_stdint.h"
|
||||
|
||||
namespace drivers {
|
||||
|
||||
namespace pcm512x {
|
||||
class Register {
|
||||
public:
|
||||
uint8_t page;
|
||||
uint8_t reg;
|
||||
|
||||
constexpr Register(uint8_t p, uint8_t r) : page(p), reg(r) {}
|
||||
};
|
||||
|
||||
constexpr Register RESET(0, 1);
|
||||
constexpr Register POWER(0, 2);
|
||||
constexpr Register MUTE(0, 3);
|
||||
constexpr Register PLL_EN(0, 4);
|
||||
constexpr Register SPI_MISO_FUNCTION(0, 6);
|
||||
constexpr Register DSP(0, 7);
|
||||
constexpr Register GPIO_EN(0, 8);
|
||||
constexpr Register BCLK_LRCLK_CFG(0, 9);
|
||||
constexpr Register DSP_GPIO_INPUT(0, 10);
|
||||
constexpr Register MASTER_MODE(0, 12);
|
||||
constexpr Register PLL_REF(0, 13);
|
||||
constexpr Register DAC_REF(0, 14);
|
||||
constexpr Register GPIO_DACIN(0, 16);
|
||||
constexpr Register GPIO_PLLIN(0, 18);
|
||||
constexpr Register SYNCHRONIZE(0, 19);
|
||||
constexpr Register PLL_COEFF_0(0, 20);
|
||||
constexpr Register PLL_COEFF_1(0, 21);
|
||||
constexpr Register PLL_COEFF_2(0, 22);
|
||||
constexpr Register PLL_COEFF_3(0, 23);
|
||||
constexpr Register PLL_COEFF_4(0, 24);
|
||||
constexpr Register DSP_CLKDIV(0, 27);
|
||||
constexpr Register DAC_CLKDIV(0, 28);
|
||||
constexpr Register NCP_CLKDIV(0, 29);
|
||||
constexpr Register OSR_CLKDIV(0, 30);
|
||||
constexpr Register MASTER_CLKDIV_1(0, 32);
|
||||
constexpr Register MASTER_CLKDIV_2(0, 33);
|
||||
constexpr Register FS_SPEED_MODE(0, 34);
|
||||
constexpr Register IDAC_1(0, 35);
|
||||
constexpr Register IDAC_2(0, 36);
|
||||
constexpr Register ERROR_DETECT(0, 37);
|
||||
constexpr Register I2S_1(0, 40);
|
||||
constexpr Register I2S_2(0, 41);
|
||||
constexpr Register DAC_ROUTING(0, 42);
|
||||
constexpr Register DSP_PROGRAM(0, 43);
|
||||
constexpr Register CLKDET(0, 44);
|
||||
constexpr Register AUTO_MUTE(0, 59);
|
||||
constexpr Register DIGITAL_VOLUME_1(0, 60);
|
||||
constexpr Register DIGITAL_VOLUME_2(0, 61);
|
||||
constexpr Register DIGITAL_VOLUME_3(0, 62);
|
||||
constexpr Register DIGITAL_MUTE_1(0, 63);
|
||||
constexpr Register DIGITAL_MUTE_2(0, 64);
|
||||
constexpr Register DIGITAL_MUTE_3(0, 65);
|
||||
constexpr Register GPIO_OUTPUT_1(0, 80);
|
||||
constexpr Register GPIO_OUTPUT_2(0, 81);
|
||||
constexpr Register GPIO_OUTPUT_3(0, 82);
|
||||
constexpr Register GPIO_OUTPUT_4(0, 83);
|
||||
constexpr Register GPIO_OUTPUT_5(0, 84);
|
||||
constexpr Register GPIO_OUTPUT_6(0, 85);
|
||||
constexpr Register GPIO_CONTROL_1(0, 86);
|
||||
constexpr Register GPIO_CONTROL_2(0, 87);
|
||||
constexpr Register OVERFLOW(0, 90);
|
||||
constexpr Register RATE_DET_1(0, 91);
|
||||
constexpr Register RATE_DET_2(0, 92);
|
||||
constexpr Register RATE_DET_3(0, 93);
|
||||
constexpr Register RATE_DET_4(0, 94);
|
||||
constexpr Register CLOCK_STATUS(0, 95);
|
||||
constexpr Register ANALOG_MUTE_DET(0, 108);
|
||||
constexpr Register POWER_STATE(0, 118);
|
||||
constexpr Register GPIN(0, 119);
|
||||
constexpr Register DIGITAL_MUTE_DET(0, 120);
|
||||
|
||||
constexpr Register OUTPUT_AMPLITUDE(1, 1);
|
||||
constexpr Register ANALOG_GAIN_CTRL(1, 2);
|
||||
constexpr Register UNDERVOLTAGE_PROT(1, 5);
|
||||
constexpr Register ANALOG_MUTE_CTRL(1, 6);
|
||||
constexpr Register ANALOG_GAIN_BOOST(1, 7);
|
||||
constexpr Register VCOM_CTRL_1(1, 8);
|
||||
constexpr Register VCOM_CTRL_2(1, 9);
|
||||
|
||||
constexpr Register CRAM_CTRL(44, 1);
|
||||
|
||||
constexpr Register FLEX_A(253, 63);
|
||||
constexpr Register FLEX_B(253, 64);
|
||||
|
||||
} // namespace pcm512x
|
||||
|
||||
/**
|
||||
* Interface for a PCM5122PWR DAC, configured over I2C.
|
||||
*/
|
||||
class AudioDac {
|
||||
public:
|
||||
enum Error {
|
||||
FAILED_TO_BOOT,
|
||||
FAILED_TO_CONFIGURE,
|
||||
FAILED_TO_INSTALL_I2S,
|
||||
};
|
||||
|
||||
static auto create(GpioExpander* expander) -> cpp::result<AudioDac*, Error>;
|
||||
|
||||
AudioDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle);
|
||||
~AudioDac();
|
||||
|
||||
/**
|
||||
* Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of
|
||||
* 255 engages the soft mute function.
|
||||
*/
|
||||
void WriteVolume(uint8_t volume);
|
||||
|
||||
enum PowerState {
|
||||
POWERDOWN = 0b0,
|
||||
WAIT_FOR_CP = 0b1,
|
||||
CALIBRATION_1 = 0b10,
|
||||
CALIBRATION_2 = 0b11,
|
||||
RAMP_UP = 0b100,
|
||||
RUN = 0b101,
|
||||
SHORT = 0b110,
|
||||
RAMP_DOWN = 0b111,
|
||||
STANDBY = 0b1000,
|
||||
};
|
||||
|
||||
/* Returns the current boot-up status and internal state of the DAC */
|
||||
std::pair<bool, PowerState> ReadPowerState();
|
||||
|
||||
enum BitsPerSample {
|
||||
BPS_16 = I2S_DATA_BIT_WIDTH_16BIT,
|
||||
BPS_24 = I2S_DATA_BIT_WIDTH_24BIT,
|
||||
BPS_32 = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
};
|
||||
enum SampleRate {
|
||||
SAMPLE_RATE_11_025 = 11025,
|
||||
SAMPLE_RATE_16 = 16000,
|
||||
SAMPLE_RATE_22_05 = 22050,
|
||||
SAMPLE_RATE_32 = 32000,
|
||||
SAMPLE_RATE_44_1 = 44100,
|
||||
SAMPLE_RATE_48 = 48000,
|
||||
SAMPLE_RATE_96 = 96000,
|
||||
SAMPLE_RATE_192 = 192000,
|
||||
};
|
||||
|
||||
// TODO(jacqueline): worth supporting channels here as well?
|
||||
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
|
||||
|
||||
auto WriteData(const cpp::span<const std::byte>& data) -> void;
|
||||
auto SetSource(StreamBufferHandle_t buffer) -> void;
|
||||
|
||||
auto Stop() -> void;
|
||||
auto LogStatus() -> void;
|
||||
|
||||
// Not copyable or movable.
|
||||
AudioDac(const AudioDac&) = delete;
|
||||
AudioDac& operator=(const AudioDac&) = delete;
|
||||
|
||||
private:
|
||||
GpioExpander* gpio_;
|
||||
i2s_chan_handle_t i2s_handle_;
|
||||
bool i2s_active_;
|
||||
std::optional<uint8_t> active_page_;
|
||||
|
||||
i2s_std_clk_config_t clock_config_;
|
||||
i2s_std_slot_config_t slot_config_;
|
||||
|
||||
/*
|
||||
* Pools the power state for up to 10ms, waiting for the given predicate to
|
||||
* be true.
|
||||
*/
|
||||
bool WaitForPowerState(std::function<bool(bool, PowerState)> predicate);
|
||||
|
||||
void WriteRegister(pcm512x::Register r, uint8_t val);
|
||||
uint8_t ReadRegister(pcm512x::Register r);
|
||||
|
||||
void SelectPage(uint8_t page);
|
||||
void WriteRegisterRaw(uint8_t reg, uint8_t val);
|
||||
uint8_t ReadRegisterRaw(uint8_t reg);
|
||||
};
|
||||
|
||||
} // namespace drivers
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <functional>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "result.hpp"
|
||||
|
||||
#include "gpio_expander.hpp"
|
||||
|
||||
namespace drivers {
|
||||
|
||||
/*
|
||||
* Driver for a two-channel digital potentiometer, with steps measured in
|
||||
* decibels.
|
||||
*/
|
||||
class DigitalPot {
|
||||
public:
|
||||
explicit DigitalPot(GpioExpander* gpios);
|
||||
~DigitalPot() {}
|
||||
|
||||
// Not copyable or movable.
|
||||
DigitalPot(const DigitalPot&) = delete;
|
||||
DigitalPot& operator=(const DigitalPot&) = delete;
|
||||
|
||||
enum class Channel {
|
||||
kLeft,
|
||||
kRight,
|
||||
};
|
||||
|
||||
auto SetRelative(int_fast8_t change) -> void;
|
||||
auto SetRelative(Channel ch, int_fast8_t change) -> void;
|
||||
|
||||
auto SetZeroCrossDetect(bool enabled) -> void;
|
||||
|
||||
auto GetMaxAttenuation() -> int_fast8_t;
|
||||
auto GetMinAttenuation() -> int_fast8_t;
|
||||
|
||||
private:
|
||||
GpioExpander* gpios_;
|
||||
};
|
||||
|
||||
} // namespace drivers
|
||||
@@ -56,12 +56,12 @@ class GpioExpander {
|
||||
static const uint8_t kPortADefault = 0b10111110;
|
||||
|
||||
// Port B:
|
||||
// 0 - trs output enable
|
||||
// 1 - 3.5mm jack detect (active low)
|
||||
// 2 - NC
|
||||
// 3 - NC
|
||||
// 4 - NC
|
||||
// 5 - NC
|
||||
// 0 - 3.5mm jack detect (active low)
|
||||
// 1 - trs output enable
|
||||
// 2 - volume zero-cross detection
|
||||
// 3 - volume direction
|
||||
// 4 - volume left channel
|
||||
// 5 - volume right channel
|
||||
// 6 - NC
|
||||
// 7 - NC
|
||||
// Default input high, trs output low
|
||||
@@ -118,12 +118,12 @@ class GpioExpander {
|
||||
SD_CARD_POWER_ENABLE = 7,
|
||||
|
||||
// Port B
|
||||
AMP_EN = 8,
|
||||
PHONE_DETECT = 9,
|
||||
// UNUSED = 10,
|
||||
// UNUSED = 11,
|
||||
// UNUSED = 12,
|
||||
// UNUSED = 13,
|
||||
PHONE_DETECT = 8,
|
||||
AMP_EN = 9,
|
||||
VOL_Z_CROSS = 10,
|
||||
VOL_UP_DOWN = 11,
|
||||
VOL_LEFT = 12,
|
||||
VOL_RIGHT = 13,
|
||||
// UNUSED = 14,
|
||||
// UNUSED = 15,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2023 jacqueline <me@jacqueline.id.au>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "driver/i2s_std.h"
|
||||
#include "driver/i2s_types.h"
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/stream_buffer.h"
|
||||
#include "result.hpp"
|
||||
#include "span.hpp"
|
||||
|
||||
#include "gpio_expander.hpp"
|
||||
#include "sys/_stdint.h"
|
||||
|
||||
namespace drivers {
|
||||
|
||||
/**
|
||||
* Interface for a DAC that receives PCM samples over I2S.
|
||||
*/
|
||||
class I2SDac {
|
||||
public:
|
||||
static auto create(GpioExpander* expander) -> std::optional<I2SDac*>;
|
||||
|
||||
I2SDac(GpioExpander* gpio, i2s_chan_handle_t i2s_handle);
|
||||
~I2SDac();
|
||||
|
||||
auto Start() -> void;
|
||||
auto Stop() -> void;
|
||||
|
||||
enum BitsPerSample {
|
||||
BPS_16 = I2S_DATA_BIT_WIDTH_16BIT,
|
||||
BPS_24 = I2S_DATA_BIT_WIDTH_24BIT,
|
||||
BPS_32 = I2S_DATA_BIT_WIDTH_32BIT,
|
||||
};
|
||||
enum SampleRate {
|
||||
SAMPLE_RATE_11_025 = 11025,
|
||||
SAMPLE_RATE_16 = 16000,
|
||||
SAMPLE_RATE_22_05 = 22050,
|
||||
SAMPLE_RATE_32 = 32000,
|
||||
SAMPLE_RATE_44_1 = 44100,
|
||||
SAMPLE_RATE_48 = 48000,
|
||||
SAMPLE_RATE_96 = 96000,
|
||||
SAMPLE_RATE_192 = 192000,
|
||||
};
|
||||
|
||||
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
|
||||
|
||||
auto WriteData(const cpp::span<const std::byte>& data) -> void;
|
||||
auto SetSource(StreamBufferHandle_t buffer) -> void;
|
||||
|
||||
// Not copyable or movable.
|
||||
I2SDac(const I2SDac&) = delete;
|
||||
I2SDac& operator=(const I2SDac&) = delete;
|
||||
|
||||
private:
|
||||
GpioExpander* gpio_;
|
||||
i2s_chan_handle_t i2s_handle_;
|
||||
bool i2s_active_;
|
||||
std::optional<uint8_t> active_page_;
|
||||
|
||||
i2s_std_clk_config_t clock_config_;
|
||||
i2s_std_slot_config_t slot_config_;
|
||||
};
|
||||
|
||||
} // namespace drivers
|
||||
@@ -63,14 +63,12 @@ auto Booting::entry() -> void {
|
||||
// booting. We will transition to the error state if these aren't present.
|
||||
ESP_LOGI(kTag, "installing required drivers");
|
||||
sSamd.reset(drivers::Samd::Create());
|
||||
auto dac_res = drivers::AudioDac::create(sGpioExpander.get());
|
||||
|
||||
if (dac_res.has_error() || !sSamd || !sRelativeTouch) {
|
||||
if (!sSamd || !sRelativeTouch) {
|
||||
events::Dispatch<FatalError, SystemState, ui::UiState, audio::AudioState>(
|
||||
FatalError());
|
||||
return;
|
||||
}
|
||||
sDac.reset(dac_res.value());
|
||||
|
||||
// These drivers are initialised on boot, but are recoverable (if weird) if
|
||||
// they fail.
|
||||
@@ -79,7 +77,11 @@ auto Booting::entry() -> void {
|
||||
|
||||
// All drivers are now loaded, so we can finish initing the other state
|
||||
// machines.
|
||||
audio::AudioState::Init(sGpioExpander.get(), sDac, sDatabase);
|
||||
if (!audio::AudioState::Init(sGpioExpander.get(), sDatabase)) {
|
||||
events::Dispatch<FatalError, SystemState, ui::UiState, audio::AudioState>(
|
||||
FatalError());
|
||||
return;
|
||||
}
|
||||
|
||||
events::Dispatch<BootComplete, SystemState, ui::UiState, audio::AudioState>(
|
||||
BootComplete());
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
#include "app_console.hpp"
|
||||
#include "battery.hpp"
|
||||
#include "dac.hpp"
|
||||
#include "database.hpp"
|
||||
#include "display.hpp"
|
||||
#include "gpio_expander.hpp"
|
||||
@@ -56,7 +55,6 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
|
||||
static std::shared_ptr<drivers::Battery> sBattery;
|
||||
static std::shared_ptr<drivers::SdStorage> sStorage;
|
||||
static std::shared_ptr<drivers::Display> sDisplay;
|
||||
static std::shared_ptr<drivers::AudioDac> sDac;
|
||||
static std::shared_ptr<database::Database> sDatabase;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ std::shared_ptr<drivers::RelativeWheel> SystemState::sRelativeTouch;
|
||||
std::shared_ptr<drivers::Battery> SystemState::sBattery;
|
||||
std::shared_ptr<drivers::SdStorage> SystemState::sStorage;
|
||||
std::shared_ptr<drivers::Display> SystemState::sDisplay;
|
||||
std::shared_ptr<drivers::AudioDac> SystemState::sDac;
|
||||
std::shared_ptr<database::Database> SystemState::sDatabase;
|
||||
|
||||
void SystemState::react(const FatalError& err) {
|
||||
|
||||
@@ -22,8 +22,8 @@ namespace ui {
|
||||
class UiState : public tinyfsm::Fsm<UiState> {
|
||||
public:
|
||||
static auto Init(drivers::GpioExpander* gpio_expander,
|
||||
const std::weak_ptr<drivers::RelativeWheel> &touchwheel,
|
||||
const std::weak_ptr<drivers::Display> &display) -> void;
|
||||
const std::weak_ptr<drivers::RelativeWheel>& touchwheel,
|
||||
const std::weak_ptr<drivers::Display>& display) -> void;
|
||||
|
||||
virtual ~UiState() {}
|
||||
|
||||
|
||||
+1
-2
@@ -42,8 +42,7 @@ void PreBoot::react(const system_fsm::DisplayReady& ev) {
|
||||
transit<Splash>();
|
||||
}
|
||||
|
||||
void Splash::entry() {
|
||||
}
|
||||
void Splash::entry() {}
|
||||
|
||||
void Splash::react(const system_fsm::BootComplete& ev) {
|
||||
transit<Interactive>();
|
||||
|
||||
Reference in New Issue
Block a user