R4 pre-emptive bringup

Includes stripping out the IC-specific I2S stuff, and doing more manual
volume control using pots
custom
jacqueline 2 years ago
parent d2e5d2ab3c
commit 610991455d
  1. 21
      src/audio/audio_fsm.cpp
  2. 1
      src/audio/audio_task.cpp
  3. 102
      src/audio/i2s_audio_output.cpp
  4. 10
      src/audio/include/audio_fsm.hpp
  5. 7
      src/audio/include/audio_sink.hpp
  6. 8
      src/audio/include/audio_task.hpp
  7. 20
      src/audio/include/i2s_audio_output.hpp
  8. 4
      src/drivers/CMakeLists.txt
  9. 465
      src/drivers/dac.cpp
  10. 85
      src/drivers/digital_pot.cpp
  11. 202
      src/drivers/i2s_dac.cpp
  12. 204
      src/drivers/include/dac.hpp
  13. 49
      src/drivers/include/digital_pot.hpp
  14. 24
      src/drivers/include/gpio_expander.hpp
  15. 78
      src/drivers/include/i2s_dac.hpp
  16. 10
      src/system_fsm/booting.cpp
  17. 2
      src/system_fsm/include/system_fsm.hpp
  18. 1
      src/system_fsm/system_fsm.cpp
  19. 3
      src/ui/ui_fsm.cpp

@ -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) {

@ -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>();

Loading…
Cancel
Save