WIP use result<> and RAII

custom
jacqueline 3 years ago
parent efd5392f6c
commit 4fc5f931ac
  1. 20
      main/CMakeLists.txt
  2. 6
      main/battery.cpp
  3. 2
      main/battery.h
  4. 93
      main/dac.cpp
  5. 101
      main/dac.h
  6. 219
      main/gay-ipod-fw.cpp
  7. 32
      main/gpio-expander.cpp
  8. 364
      main/gpio-expander.h
  9. 5
      main/i2c.cpp
  10. 111
      main/i2c.h
  11. 224
      main/playback.cpp
  12. 68
      main/playback.h
  13. 120
      main/storage.cpp
  14. 104
      main/storage.h

@ -1,5 +1,15 @@
idf_component_register( idf_component_register(SRCS
SRCS "gay-ipod-fw.cpp" "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" "gay-ipod-fw.cpp"
INCLUDE_DIRS "." "dac.cpp"
REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" "gpio-expander.cpp"
) "battery.cpp"
"storage.cpp"
"i2c.cpp"
"playback.cpp" INCLUDE_DIRS "." REQUIRES
"esp_adc_cal"
"fatfs"
"audio_pipeline"
"audio_stream"
"result")
target_compile_options(${COMPONENT_LIB} PRIVATE -
DRESULT_DISABLE_EXCEPTIONS)

@ -11,8 +11,8 @@ static esp_adc_cal_characteristics_t calibration;
esp_err_t init_adc(void) { esp_err_t init_adc(void) {
// Calibration should already be fused into the chip from the factory, so // Calibration should already be fused into the chip from the factory, so
// we should only need to read it back out again. // we should only need to read it back out again.
esp_adc_cal_characterize( esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0,
ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &calibration); &calibration);
// Max battery voltage should be a little over 2V due to our divider, so // Max battery voltage should be a little over 2V due to our divider, so
// we need the max attenuation to properly handle the full range. // we need the max attenuation to properly handle the full range.
@ -28,4 +28,4 @@ uint32_t read_battery_voltage(void) {
return esp_adc_cal_raw_to_voltage(raw, &calibration); return esp_adc_cal_raw_to_voltage(raw, &calibration);
} }
} // namespace gay_ipod } // namespace gay_ipod

@ -13,4 +13,4 @@ esp_err_t init_adc(void);
*/ */
uint32_t read_battery_voltage(void); uint32_t read_battery_voltage(void);
} // namespace gay_ipod } // namespace gay_ipod

@ -1,41 +1,56 @@
#include "dac.h" #include "dac.h"
#include "esp_err.h" #include <cstdint>
#include "i2c.h"
#include "esp_log.h"
#include "assert.h" #include "assert.h"
#include "driver/i2c.h" #include "driver/i2c.h"
#include "esp_err.h"
#include "esp_log.h"
#include "gpio-expander.h" #include "gpio-expander.h"
#include "hal/i2c_types.h" #include "hal/i2c_types.h"
#include <cstdint> #include "i2c.h"
namespace gay_ipod { namespace gay_ipod {
static const char* TAG = "AUDIODAC"; static const char* kTag = "AUDIODAC";
static const uint8_t kPcm5122Address = 0x4C;
AudioDac::AudioDac(GpioExpander *gpio) { static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS;
this->gpio_ = gpio;
};
AudioDac::~AudioDac() {}; auto AudioDac::create(GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioDac>, Error> {
std::unique_ptr<AudioDac> dac = std::make_unique<AudioDac>(expander);
esp_err_t AudioDac::Start() { bool is_booted = dac->WaitForPowerState(
bool is_booted = WaitForPowerState([](bool booted, PowerState state){ return booted; }); [](bool booted, PowerState state) { return booted; });
if (!is_booted) { if (!is_booted) {
ESP_LOGE(TAG, "Timed out waiting for boot"); ESP_LOGE(kTag, "Timed out waiting for boot");
return ESP_ERR_TIMEOUT; return cpp::fail(Error::FAILED_TO_BOOT);
} }
WriteRegister(Register::DE_EMPHASIS, 1 << 4); dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4);
WriteVolume(100); dac->WriteVolume(100);
WaitForPowerState([](bool booted, PowerState state){ bool is_configured =
return state == WAIT_FOR_CP || state == RAMP_UP || state == RUN || state == STANDBY; dac->WaitForPowerState([](bool booted, PowerState state) {
}); return state == WAIT_FOR_CP || state == RAMP_UP || state == RUN ||
state == STANDBY;
});
if (!is_configured) {
return cpp::fail(Error::FAILED_TO_CONFIGURE);
}
return ESP_OK; return dac;
} }
AudioDac::AudioDac(GpioExpander* gpio) {
this->gpio_ = gpio;
};
AudioDac::~AudioDac(){
// TODO: reset stuff like de-emphasis? Reboot the whole dac? Need to think
// about this.
};
void AudioDac::WriteVolume(uint8_t volume) { void AudioDac::WriteVolume(uint8_t volume) {
WriteRegister(Register::DIGITAL_VOLUME_L, volume); WriteRegister(Register::DIGITAL_VOLUME_L, volume);
WriteRegister(Register::DIGITAL_VOLUME_R, volume); WriteRegister(Register::DIGITAL_VOLUME_R, volume);
@ -45,31 +60,32 @@ std::pair<bool, AudioDac::PowerState> AudioDac::ReadPowerState() {
uint8_t result = 0; uint8_t result = 0;
I2CTransaction transaction; I2CTransaction transaction;
transaction transaction.start()
.start() .write_addr(kPcm5122Address, I2C_MASTER_WRITE)
.write_addr(kPCM5122Address, I2C_MASTER_WRITE) .write_ack(DSP_BOOT_POWER_STATE)
.write_ack(DSP_BOOT_POWER_STATE) .start()
.start() .write_addr(kPcm5122Address, I2C_MASTER_READ)
.write_addr(kPCM5122Address, I2C_MASTER_READ) .read(&result, I2C_MASTER_NACK)
.read(&result, I2C_MASTER_NACK) .stop();
.stop();
ESP_ERROR_CHECK(transaction.Execute()); ESP_ERROR_CHECK(transaction.Execute());
bool is_booted = result >> 7; bool is_booted = result >> 7;
PowerState detail = (PowerState) (result & 0b1111); PowerState detail = (PowerState)(result & 0b1111);
return std::pair(is_booted, detail); return std::pair(is_booted, detail);
} }
bool AudioDac::WaitForPowerState(std::function<bool(bool,AudioDac::PowerState)> predicate) { bool AudioDac::WaitForPowerState(
std::function<bool(bool, AudioDac::PowerState)> predicate) {
bool has_matched = false; bool has_matched = false;
for (int i=0; i<10; i++) { for (int i = 0; i < 10; i++) {
std::pair<bool, PowerState> result = ReadPowerState(); std::pair<bool, PowerState> result = ReadPowerState();
has_matched = predicate(result.first, result.second); has_matched = predicate(result.first, result.second);
if (has_matched) { if (has_matched) {
break; break;
} else { } else {
ESP_LOGI(TAG, "Waiting for power state (was %d %x)", result.first, (uint8_t) result.second); ESP_LOGI(kTag, "Waiting for power state (was %d %x)", result.first,
(uint8_t)result.second);
vTaskDelay(pdMS_TO_TICKS(1)); vTaskDelay(pdMS_TO_TICKS(1));
} }
} }
@ -77,14 +93,13 @@ bool AudioDac::WaitForPowerState(std::function<bool(bool,AudioDac::PowerState)>
} }
void AudioDac::WriteRegister(Register reg, uint8_t val) { void AudioDac::WriteRegister(Register reg, uint8_t val) {
I2CTransaction transaction; I2CTransaction transaction;
transaction transaction.start()
.start() .write_addr(kPcm5122Address, I2C_MASTER_WRITE)
.write_addr(kPCM5122Address, I2C_MASTER_WRITE)
.write_ack(reg, val) .write_ack(reg, val)
.stop(); .stop();
// TODO: Retry once? // TODO: Retry once?
ESP_ERROR_CHECK(transaction.Execute()); ESP_ERROR_CHECK(transaction.Execute());
} }
} // namespace gay_ipod } // namespace gay_ipod

@ -1,72 +1,73 @@
#pragma once #pragma once
#include "esp_err.h"
#include "gpio-expander.h"
#include <stdint.h> #include <stdint.h>
#include <functional> #include <functional>
namespace gay_ipod { #include "esp_err.h"
#include "gpio-expander.h"
#include "result.hpp"
static const uint8_t kPCM5122Address = 0x4C; namespace gay_ipod {
static const uint8_t kPCM5122Timeout = 100 / portTICK_RATE_MS;
/** /**
* Interface for a PCM5122PWR DAC, configured over I2C. * Interface for a PCM5122PWR DAC, configured over I2C.
*/ */
class AudioDac { class AudioDac {
public: public:
AudioDac(GpioExpander *gpio); enum Error {
~AudioDac(); FAILED_TO_BOOT,
FAILED_TO_CONFIGURE,
};
static auto create(GpioExpander* expander)
-> cpp::result<std::unique_ptr<AudioDac>, Error>;
/** AudioDac(GpioExpander* gpio);
* Performs initial configuration of the DAC and sets it to begin expecting ~AudioDac();
* I2S audio data.
*/
esp_err_t Start();
/** /**
* Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of * Sets the volume on a scale from 0 (loudest) to 254 (quietest). A value of
* 255 engages the soft mute function. * 255 engages the soft mute function.
*/ */
void WriteVolume(uint8_t volume); void WriteVolume(uint8_t volume);
enum PowerState { enum PowerState {
POWERDOWN = 0b0, POWERDOWN = 0b0,
WAIT_FOR_CP = 0b1, WAIT_FOR_CP = 0b1,
CALIBRATION_1 = 0b10, CALIBRATION_1 = 0b10,
CALIBRATION_2 = 0b11, CALIBRATION_2 = 0b11,
RAMP_UP = 0b100, RAMP_UP = 0b100,
RUN = 0b101, RUN = 0b101,
SHORT = 0b110, SHORT = 0b110,
RAMP_DOWN = 0b111, RAMP_DOWN = 0b111,
STANDBY = 0b1000, STANDBY = 0b1000,
}; };
/* Returns the current boot-up status and internal state of the DAC */ /* Returns the current boot-up status and internal state of the DAC */
std::pair<bool,PowerState> ReadPowerState(); std::pair<bool, PowerState> ReadPowerState();
// Not copyable or movable. // Not copyable or movable.
AudioDac(const AudioDac&) = delete; AudioDac(const AudioDac&) = delete;
AudioDac& operator=(const AudioDac&) = delete; AudioDac& operator=(const AudioDac&) = delete;
private: private:
GpioExpander *gpio_; GpioExpander* gpio_;
/* /*
* Pools the power state for up to 10ms, waiting for the given predicate to * Pools the power state for up to 10ms, waiting for the given predicate to
* be true. * be true.
*/ */
bool WaitForPowerState(std::function<bool(bool,PowerState)> predicate); bool WaitForPowerState(std::function<bool(bool, PowerState)> predicate);
enum Register { enum Register {
PAGE_SELECT = 0, PAGE_SELECT = 0,
DE_EMPHASIS = 7, DE_EMPHASIS = 7,
DIGITAL_VOLUME_L = 61, DIGITAL_VOLUME_L = 61,
DIGITAL_VOLUME_R = 62, DIGITAL_VOLUME_R = 62,
DSP_BOOT_POWER_STATE = 118, DSP_BOOT_POWER_STATE = 118,
}; };
void WriteRegister(Register reg, uint8_t val); void WriteRegister(Register reg, uint8_t val);
}; };
} // namespace gay_ipod } // namespace gay_ipod

@ -1,7 +1,12 @@
#include <cstdint>
#include <stdio.h>
#include <dirent.h> #include <dirent.h>
#include <stdio.h>
#include <cstdint>
#include <memory>
#include "audio_common.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "battery.h" #include "battery.h"
#include "dac.h" #include "dac.h"
#include "driver/adc.h" #include "driver/adc.h"
@ -14,19 +19,16 @@
#include "esp_adc_cal.h" #include "esp_adc_cal.h"
#include "esp_intr_alloc.h" #include "esp_intr_alloc.h"
#include "esp_log.h" #include "esp_log.h"
#include "fatfs_stream.h"
#include "gpio-expander.h" #include "gpio-expander.h"
#include "hal/adc_types.h" #include "hal/adc_types.h"
#include "hal/gpio_types.h" #include "hal/gpio_types.h"
#include "hal/i2s_types.h" #include "hal/i2s_types.h"
#include "hal/spi_types.h" #include "hal/spi_types.h"
#include "storage.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_common.h"
#include "fatfs_stream.h"
#include "i2s_stream.h" #include "i2s_stream.h"
#include "mp3_decoder.h" #include "mp3_decoder.h"
#include "playback.h"
#include "storage.h"
#define I2C_SDA_IO (GPIO_NUM_2) #define I2C_SDA_IO (GPIO_NUM_2)
#define I2C_SCL_IO (GPIO_NUM_4) #define I2C_SCL_IO (GPIO_NUM_4)
@ -48,16 +50,17 @@ static const char* TAG = "MAIN";
esp_err_t init_i2c(void) { esp_err_t init_i2c(void) {
i2c_port_t port = I2C_NUM_0; i2c_port_t port = I2C_NUM_0;
i2c_config_t config = { i2c_config_t config = {
.mode = I2C_MODE_MASTER, .mode = I2C_MODE_MASTER,
.sda_io_num = I2C_SDA_IO, .sda_io_num = I2C_SDA_IO,
.scl_io_num = I2C_SCL_IO, .scl_io_num = I2C_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE, .sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE,
.master = { .master =
.clk_speed = I2C_CLOCK_HZ, {
}, .clk_speed = I2C_CLOCK_HZ,
// No requirements for the clock. },
.clk_flags = 0, // No requirements for the clock.
.clk_flags = 0,
}; };
ESP_ERROR_CHECK(i2c_param_config(port, &config)); ESP_ERROR_CHECK(i2c_param_config(port, &config));
@ -70,22 +73,22 @@ esp_err_t init_i2c(void) {
esp_err_t init_spi(void) { esp_err_t init_spi(void) {
spi_bus_config_t config = { spi_bus_config_t config = {
.mosi_io_num = SPI_SDO_IO, .mosi_io_num = SPI_SDO_IO,
.miso_io_num = SPI_SDI_IO, .miso_io_num = SPI_SDI_IO,
.sclk_io_num = SPI_SCLK_IO, .sclk_io_num = SPI_SCLK_IO,
.quadwp_io_num = SPI_QUADWP_IO, .quadwp_io_num = SPI_QUADWP_IO,
.quadhd_io_num = SPI_QUADHD_IO, .quadhd_io_num = SPI_QUADHD_IO,
// Unused // Unused
.data4_io_num = -1, .data4_io_num = -1,
.data5_io_num = -1, .data5_io_num = -1,
.data6_io_num = -1, .data6_io_num = -1,
.data7_io_num = -1, .data7_io_num = -1,
// Use the DMA default size. // Use the DMA default size.
.max_transfer_sz = 0, .max_transfer_sz = 0,
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS,
.intr_flags = 0, .intr_flags = 0,
}; };
ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &config, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &config, SPI_DMA_CH_AUTO));
@ -93,8 +96,7 @@ esp_err_t init_spi(void) {
return ESP_OK; return ESP_OK;
} }
extern "C" void app_main(void) extern "C" void app_main(void) {
{
ESP_LOGI(TAG, "Initialising peripherals"); ESP_LOGI(TAG, "Initialising peripherals");
ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED)); ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED));
@ -105,140 +107,45 @@ extern "C" void app_main(void)
gay_ipod::GpioExpander expander; gay_ipod::GpioExpander expander;
// for debugging usb ic // for debugging usb ic
//expander.set_sd_mux(gay_ipod::GpioExpander::USB); // expander.set_sd_mux(gay_ipod::GpioExpander::USB);
ESP_LOGI(TAG, "Init ADC"); ESP_LOGI(TAG, "Init ADC");
ESP_ERROR_CHECK(gay_ipod::init_adc()); ESP_ERROR_CHECK(gay_ipod::init_adc());
ESP_LOGI(TAG, "Init SD card"); ESP_LOGI(TAG, "Init SD card");
gay_ipod::SdStorage storage(&expander); auto storage_res = gay_ipod::SdStorage::create(&expander);
gay_ipod::SdStorage::Error err = storage.Acquire(); if (storage_res.has_error()) {
if (err != gay_ipod::SdStorage::Error::OK) { ESP_LOGE(TAG, "Failed: %d", storage_res.error());
ESP_LOGE(TAG, "Failed to acquire storage!");
return; return;
} }
std::unique_ptr<gay_ipod::SdStorage> storage = std::move(storage_res.value());
ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); ESP_LOGI(TAG, "Init DAC");
vTaskDelay(pdMS_TO_TICKS(1500)); auto dac_res = gay_ipod::AudioDac::create(&expander);
if (storage_res.has_error()) {
ESP_LOGI(TAG, "Trying to init DAC"); ESP_LOGE(TAG, "Failed: %d", dac_res.error());
gay_ipod::AudioDac dac(&expander); return;
dac.Start(); }
std::unique_ptr<gay_ipod::AudioDac> dac = std::move(dac_res.value());
vTaskDelay(pdMS_TO_TICKS(1000));
ESP_LOGI(TAG, "Looks okay? Let's play some music!");
i2s_port_t port = I2S_NUM_0;
i2s_config_t i2s_config = {
// Weird enum usage in ESP IDF.
.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 44100,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
};
//ESP_ERROR_CHECK(i2s_driver_install(port, &i2s_config, 0, NULL));
audio_pipeline_handle_t pipeline;
audio_element_handle_t fatfs_stream_reader, i2s_stream_writer, audio_decoder;
audio_pipeline_cfg_t pipeline_config = audio_pipeline_cfg_t(DEFAULT_AUDIO_PIPELINE_CONFIG());
pipeline = audio_pipeline_init(&pipeline_config);
assert(pipeline != NULL);
ESP_LOGI(TAG, "Made pipeline okay.");
fatfs_stream_cfg_t fatfs_stream_config = fatfs_stream_cfg_t(FATFS_STREAM_CFG_DEFAULT());
fatfs_stream_config.type = AUDIO_STREAM_READER;
fatfs_stream_reader = fatfs_stream_init(&fatfs_stream_config);
assert(fatfs_stream_reader != NULL);
ESP_LOGI(TAG, "Made reader okay.");
i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{
.type = AUDIO_STREAM_WRITER,
.i2s_config = i2s_config,
.i2s_port = port,
.use_alc = false,
.volume = 0, // Does nothing; use the dac
.out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
.task_stack = I2S_STREAM_TASK_STACK,
.task_core = I2S_STREAM_TASK_CORE,
.task_prio = I2S_STREAM_TASK_PRIO,
.stack_in_ext = false,
.multi_out_num = 0,
.uninstall_drv = true,
.need_expand = false,
.expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
};
// TODO fix hardcoded mclk :(
i2s_stream_writer = i2s_stream_init(&i2s_stream_config);
assert(i2s_stream_writer != NULL);
ESP_LOGI(TAG, "Made i2s stream okay.");
ESP_LOGI(TAG, "Init i2s pins");
i2s_pin_config_t pin_config = {
.mck_io_num = GPIO_NUM_0,
.bck_io_num = GPIO_NUM_26,
.ws_io_num = GPIO_NUM_27,
.data_out_num = GPIO_NUM_5,
.data_in_num = I2S_PIN_NO_CHANGE
};
ESP_ERROR_CHECK(i2s_set_pin(port, &pin_config));
mp3_decoder_cfg_t decoder_config = mp3_decoder_cfg_t(DEFAULT_MP3_DECODER_CONFIG());
audio_decoder = mp3_decoder_init(&decoder_config);
assert(audio_decoder != NULL);
ESP_LOGI(TAG, "Made mp3 decoder okay.");
vTaskDelay(pdMS_TO_TICKS(500));
ESP_LOGI(TAG, "Putting it all together");
audio_pipeline_register(pipeline, fatfs_stream_reader, "file");
audio_pipeline_register(pipeline, audio_decoder, "dec");
audio_pipeline_register(pipeline, i2s_stream_writer, "i2s");
const char *link_tag[3] = {"file", "dec", "i2s"};
audio_pipeline_link(pipeline, &link_tag[0], 3);
ESP_LOGI(TAG, "Trying to play something??"); ESP_LOGI(TAG, "Init Audio Pipeline");
audio_element_set_uri(fatfs_stream_reader, "/sdcard/test.mp3"); auto playback_res = gay_ipod::DacAudioPlayback::create(dac.get());
audio_pipeline_run(pipeline); if (playback_res.has_error()) {
ESP_LOGE(TAG, "Failed: %d", playback_res.error());
return;
}
std::unique_ptr<gay_ipod::DacAudioPlayback> playback =
std::move(playback_res.value());
vTaskDelay(pdMS_TO_TICKS(1000)); ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger.");
ESP_LOGI(TAG, "It could be playing? idk"); vTaskDelay(pdMS_TO_TICKS(1500));
gay_ipod::AudioDac::PowerState state = dac.ReadPowerState().second; playback->Play("/sdcard/test.mp3");
ESP_LOGI(TAG, "dac power state: %x", (uint8_t)state); playback->set_volume(100);
vTaskDelay(pdMS_TO_TICKS(120000)); playback->WaitForSongEnd();
ESP_LOGI(TAG, "Time to deinit."); ESP_LOGI(TAG, "Time to deinit.");
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_terminate(pipeline);
audio_pipeline_unregister(pipeline, fatfs_stream_reader);
audio_pipeline_unregister(pipeline, audio_decoder);
audio_pipeline_unregister(pipeline, i2s_stream_writer);
audio_pipeline_deinit(pipeline);
audio_element_deinit(fatfs_stream_reader);
audio_element_deinit(i2s_stream_writer);
audio_element_deinit(audio_decoder);
storage.Release();
ESP_LOGI(TAG, "Hooray!"); ESP_LOGI(TAG, "Hooray!");
} }

@ -1,9 +1,9 @@
#include "gpio-expander.h" #include "gpio-expander.h"
#include "i2c.h"
#include <cstdint> #include <cstdint>
#include "i2c.h"
namespace gay_ipod { namespace gay_ipod {
GpioExpander::GpioExpander() { GpioExpander::GpioExpander() {
@ -32,9 +32,9 @@ esp_err_t GpioExpander::Write() {
I2CTransaction transaction; I2CTransaction transaction;
transaction.start() transaction.start()
.write_addr(kPca8575Address, I2C_MASTER_WRITE) .write_addr(kPca8575Address, I2C_MASTER_WRITE)
.write_ack(ports_ab.first, ports_ab.second) .write_ack(ports_ab.first, ports_ab.second)
.stop(); .stop();
return transaction.Execute(); return transaction.Execute();
} }
@ -44,10 +44,10 @@ esp_err_t GpioExpander::Read() {
I2CTransaction transaction; I2CTransaction transaction;
transaction.start() transaction.start()
.write_addr(kPca8575Address, I2C_MASTER_READ) .write_addr(kPca8575Address, I2C_MASTER_READ)
.read(&input_a, I2C_MASTER_ACK) .read(&input_a, I2C_MASTER_ACK)
.read(&input_b, I2C_MASTER_LAST_NACK) .read(&input_b, I2C_MASTER_LAST_NACK)
.stop(); .stop();
esp_err_t ret = transaction.Execute(); esp_err_t ret = transaction.Execute();
inputs_ = pack(input_a, input_b); inputs_ = pack(input_a, input_b);
@ -55,7 +55,7 @@ esp_err_t GpioExpander::Read() {
} }
void GpioExpander::set_pin(ChipSelect cs, bool value) { void GpioExpander::set_pin(ChipSelect cs, bool value) {
set_pin((Pin) cs, value); set_pin((Pin)cs, value);
} }
void GpioExpander::set_pin(Pin pin, bool value) { void GpioExpander::set_pin(Pin pin, bool value) {
@ -75,16 +75,12 @@ GpioExpander::SpiLock GpioExpander::AcquireSpiBus(ChipSelect cs) {
} }
GpioExpander::SpiLock::SpiLock(GpioExpander& gpio, ChipSelect cs) GpioExpander::SpiLock::SpiLock(GpioExpander& gpio, ChipSelect cs)
: lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) { : lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) {
gpio_.with([&](auto& gpio) { gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 0); });
gpio.set_pin(cs_, 0);
});
} }
GpioExpander::SpiLock::~SpiLock() { GpioExpander::SpiLock::~SpiLock() {
gpio_.with([&](auto& gpio) { gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 1); });
gpio.set_pin(cs_, 1);
});
} }
} // namespace gay_ipod } // namespace gay_ipod

@ -1,16 +1,17 @@
#pragma once #pragma once
#include <stdint.h>
#include <atomic> #include <atomic>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <stdint.h>
#include <tuple> #include <tuple>
#include <utility> #include <utility>
#include "driver/i2c.h"
#include "esp_check.h" #include "esp_check.h"
#include "esp_log.h"
#include "esp_err.h" #include "esp_err.h"
#include "driver/i2c.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
namespace gay_ipod { namespace gay_ipod {
@ -26,184 +27,183 @@ namespace gay_ipod {
* should be done whilst holding `cs_lock` (preferably via the helper methods). * should be done whilst holding `cs_lock` (preferably via the helper methods).
*/ */
class GpioExpander { class GpioExpander {
public: public:
GpioExpander(); GpioExpander();
~GpioExpander(); ~GpioExpander();
static const uint8_t kPca8575Address = 0x20; static const uint8_t kPca8575Address = 0x20;
static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS; static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS;
// Port A: // Port A:
// 0 - audio power enable // 0 - audio power enable
// 1 - usb interface power enable // 1 - usb interface power enable
// 2 - display power enable // 2 - display power enable
// 3 - sd card power enable // 3 - sd card power enable
// 4 - charge power ok (active low) // 4 - charge power ok (active low)
// 5 - sd mux switch // 5 - sd mux switch
// 6 - sd chip select // 6 - sd chip select
// 7 - display chip select // 7 - display chip select
// All power switches low, chip selects high, active-low charge power high // All power switches low, chip selects high, active-low charge power high
static const uint8_t kPortADefault = 0b11010001; static const uint8_t kPortADefault = 0b11010001;
// Port B: // Port B:
// 0 - 3.5mm jack detect (active low) // 0 - 3.5mm jack detect (active low)
// 1 - dac soft mute switch // 1 - dac soft mute switch
// 2 - GPIO // 2 - GPIO
// 3 - GPIO // 3 - GPIO
// 4 - GPIO // 4 - GPIO
// 5 - GPIO // 5 - GPIO
// 6 - GPIO // 6 - GPIO
// 7 - GPIO // 7 - GPIO
// DAC mute output low, everything else is active-low inputs. // DAC mute output low, everything else is active-low inputs.
static const uint8_t kPortBDefault = 0b11111111; static const uint8_t kPortBDefault = 0b11111111;
/* /*
* Convenience mehod for packing the port a and b bytes into a single 16 bit * Convenience mehod for packing the port a and b bytes into a single 16 bit
* value. * value.
*/ */
static uint16_t pack(uint8_t a, uint8_t b) { static uint16_t pack(uint8_t a, uint8_t b) { return ((uint16_t)b) << 8 | a; }
return ((uint16_t) b) << 8 | a;
} /*
* Convenience mehod for unpacking the result of `pack` back into two single
/* * byte port datas.
* Convenience mehod for unpacking the result of `pack` back into two single */
* byte port datas. static std::pair<uint8_t, uint8_t> unpack(uint16_t ba) {
*/ return std::pair((uint8_t)ba, (uint8_t)(ba >> 8));
static std::pair<uint8_t, uint8_t> unpack(uint16_t ba) { }
return std::pair((uint8_t) ba, (uint8_t) (ba >> 8));
} /*
* Convenience function for running some arbitrary pin writing code, then
/* * flushing a `Write()` to the expander. Example usage:
* Convenience function for running some arbitrary pin writing code, then *
* flushing a `Write()` to the expander. Example usage: * ```
* * gpio_.with([&](auto& gpio) {
* ``` * gpio.set_pin(AUDIO_POWER_ENABLE, true);
* gpio_.with([&](auto& gpio) { * });
* gpio.set_pin(AUDIO_POWER_ENABLE, true); * ```
* }); */
* ``` void with(std::function<void(GpioExpander&)> f);
*/
void with(std::function<void(GpioExpander&)> f); /**
* Sets the ports on the GPIO expander to the values currently represented
/** * in `ports`.
* Sets the ports on the GPIO expander to the values currently represented */
* in `ports`. esp_err_t Write(void);
*/
esp_err_t Write(void); /**
* Reads from the GPIO expander, populating `inputs` with the most recent
/** * values.
* Reads from the GPIO expander, populating `inputs` with the most recent */
* values. esp_err_t Read(void);
*/
esp_err_t Read(void); /* Maps each pin of the expander to its number in a `pack`ed uint16. */
enum Pin {
/* Maps each pin of the expander to its number in a `pack`ed uint16. */ // Port A
enum Pin { AUDIO_POWER_ENABLE = 0,
// Port A USB_INTERFACE_POWER_ENABLE = 1,
AUDIO_POWER_ENABLE = 0, DISPLAY_POWER_ENABLE = 2,
USB_INTERFACE_POWER_ENABLE = 1, SD_CARD_POWER_ENABLE = 3,
DISPLAY_POWER_ENABLE = 2, CHARGE_POWER_OK = 4, // Active-low input
SD_CARD_POWER_ENABLE = 3, SD_MUX_SWITCH = 5,
CHARGE_POWER_OK = 4, // Active-low input SD_CHIP_SELECT = 6,
SD_MUX_SWITCH = 5, DISPLAY_CHIP_SELECT = 7,
SD_CHIP_SELECT = 6,
DISPLAY_CHIP_SELECT = 7, // Port B
PHONE_DETECT = 8, // Active-high input
// Port B DAC_MUTE = 9,
PHONE_DETECT = 8, // Active-high input GPIO_1 = 10,
DAC_MUTE = 9, GPIO_2 = 11,
GPIO_1 = 10, GPIO_3 = 12,
GPIO_2 = 11, GPIO_4 = 13,
GPIO_3 = 12, GPIO_5 = 14,
GPIO_4 = 13, GPIO_6 = 15,
GPIO_5 = 14, };
GPIO_6 = 15,
}; /* Pins whose access should be guarded by `cs_lock`. */
enum ChipSelect {
/* Pins whose access should be guarded by `cs_lock`. */ SD_CARD = SD_CHIP_SELECT,
enum ChipSelect { DISPLAY = DISPLAY_CHIP_SELECT,
SD_CARD = SD_CHIP_SELECT, };
DISPLAY = DISPLAY_CHIP_SELECT,
}; /* Nicer value names for use with the SD_MUX_SWITCH pin. */
enum SdController {
/* Nicer value names for use with the SD_MUX_SWITCH pin. */ SD_MUX_ESP = 0,
enum SdController { SD_MUX_USB = 1,
SD_MUX_ESP = 0, };
SD_MUX_USB = 1,
}; /**
* Returns the current driven status of each of the ports. The first byte is
/** * port a, and the second byte is port b.
* Returns the current driven status of each of the ports. The first byte is */
* port a, and the second byte is port b. std::atomic<uint16_t>& ports() { return ports_; }
*/
std::atomic<uint16_t>& ports() { return ports_; } /*
* Sets a single specific pin to the given value. `true` corresponds to
/* * HIGH, and `false` corresponds to LOW.
* Sets a single specific pin to the given value. `true` corresponds to *
* HIGH, and `false` corresponds to LOW. * Calls to this method will be buffered in memory until a call to `Write()`
* * is made.
* Calls to this method will be buffered in memory until a call to `Write()` */
* is made. void set_pin(Pin pin, bool value);
*/ void set_pin(ChipSelect cs, bool value);
void set_pin(Pin pin, bool value);
void set_pin(ChipSelect cs, bool value); /**
* Returns the input status of each of the ports. The first byte is port a,
/** * and the second byte is port b.
* Returns the input status of each of the ports. The first byte is port a, */
* and the second byte is port b. const std::atomic<uint16_t>& inputs() const { return inputs_; }
*/
const std::atomic<uint16_t>& inputs() const { return inputs_; } /* Returns the most recently cached value of the given pin. Only valid for
* pins used as inputs; to check what value we're driving a pin, use
/* Returns the most recently cached value of the given pin. Only valid for * `ports()`.
* pins used as inputs; to check what value we're driving a pin, use */
* `ports()`. bool get_input(Pin pin) const;
*/
bool get_input(Pin pin) const; /* Returns the mutex that must be held whilst pulling a CS pin low. */
std::mutex& cs_mutex() { return cs_mutex_; }
/* Returns the mutex that must be held whilst pulling a CS pin low. */
std::mutex& cs_mutex() { return cs_mutex_; } /*
* Helper class containing an active `cs_mutex` lock. When an instance of
/* * this class is destroyed (usually by falling out of scope), the associated
* Helper class containing an active `cs_mutex` lock. When an instance of * CS pin will be driven high before the lock is released.
* this class is destroyed (usually by falling out of scope), the associated */
* CS pin will be driven high before the lock is released. class SpiLock {
*/ public:
class SpiLock { SpiLock(GpioExpander& gpio, ChipSelect cs);
public: ~SpiLock();
SpiLock(GpioExpander &gpio, ChipSelect cs);
~SpiLock(); SpiLock(const SpiLock&) = delete;
SpiLock(const SpiLock&) = delete; private:
private: std::scoped_lock<std::mutex> lock_;
std::scoped_lock<std::mutex> lock_; GpioExpander& gpio_;
GpioExpander &gpio_; ChipSelect cs_;
ChipSelect cs_; };
};
/*
/* * Pulls the given CS pin low to signal that we are about to communicate
* Pulls the given CS pin low to signal that we are about to communicate * with a particular device, after acquiring a lock on `cs_mutex`. The
* with a particular device, after acquiring a lock on `cs_mutex`. The * recommended way to safely interact with devices on the SPI bus is to have
* recommended way to safely interact with devices on the SPI bus is to have * a self-contained block like so:
* a self-contained block like so: *
* * ```
* ``` * {
* { * auto lock = AcquireSpiBus(WHATEVER);
* auto lock = AcquireSpiBus(WHATEVER); * // Do some cool things here.
* // Do some cool things here. * }
* } * ```
* ``` */
*/ SpiLock AcquireSpiBus(ChipSelect cs);
SpiLock AcquireSpiBus(ChipSelect cs);
// Not copyable or movable. There should usually only ever be once instance
// Not copyable or movable. There should usually only ever be once instance // of this class, and that instance will likely have a static lifetime.
// of this class, and that instance will likely have a static lifetime. GpioExpander(const GpioExpander&) = delete;
GpioExpander(const GpioExpander&) = delete; GpioExpander& operator=(const GpioExpander&) = delete;
GpioExpander& operator=(const GpioExpander&) = delete;
private:
private: std::mutex cs_mutex_;
std::mutex cs_mutex_; std::atomic<uint16_t> ports_;
std::atomic<uint16_t> ports_; std::atomic<uint16_t> inputs_;
std::atomic<uint16_t> inputs_;
}; };
} // namespace gay_ipod } // namespace gay_ipod

@ -1,4 +1,5 @@
#include "i2c.h" #include "i2c.h"
#include "assert.h" #include "assert.h"
namespace gay_ipod { namespace gay_ipod {
@ -36,9 +37,9 @@ I2CTransaction& I2CTransaction::write_ack(uint8_t data) {
return *this; return *this;
} }
I2CTransaction& I2CTransaction::read(uint8_t *dest, i2c_ack_type_t ack) { I2CTransaction& I2CTransaction::read(uint8_t* dest, i2c_ack_type_t ack) {
ESP_ERROR_CHECK(i2c_master_read_byte(handle_, dest, ack)); ESP_ERROR_CHECK(i2c_master_read_byte(handle_, dest, ack));
return *this; return *this;
} }
} // namespace gay_ipod } // namespace gay_ipod

@ -1,8 +1,9 @@
#pragma once #pragma once
#include <cstdint>
#include "driver/i2c.h" #include "driver/i2c.h"
#include "hal/i2c_types.h" #include "hal/i2c_types.h"
#include <cstdint>
namespace gay_ipod { namespace gay_ipod {
@ -15,66 +16,68 @@ namespace gay_ipod {
* typically represent invalid arguments or OOMs. * typically represent invalid arguments or OOMs.
*/ */
class I2CTransaction { class I2CTransaction {
public: public:
static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS; static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS;
I2CTransaction();
~I2CTransaction();
I2CTransaction(); /*
~I2CTransaction(); * Executes all enqueued commands, returning the result code. Possible error
* codes, per the ESP-IDF docs:
*
* ESP_OK Success
* ESP_ERR_INVALID_ARG Parameter error
* ESP_FAIL Sending command error, slave doesnt ACK the transfer.
* ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode.
* ESP_ERR_TIMEOUT Operation timeout because the bus is busy.
*/
esp_err_t Execute();
/* /*
* Executes all enqueued commands, returning the result code. Possible error * Enqueues a start condition. May also be used for repeated start
* codes, per the ESP-IDF docs: * conditions.
* */
* ESP_OK Success I2CTransaction& start();
* ESP_ERR_INVALID_ARG Parameter error /* Enqueues a stop condition. */
* ESP_FAIL Sending command error, slave doesnt ACK the transfer. I2CTransaction& stop();
* ESP_ERR_INVALID_STATE I2C driver not installed or not in master mode.
* ESP_ERR_TIMEOUT Operation timeout because the bus is busy.
*/
esp_err_t Execute();
/* /*
* Enqueues a start condition. May also be used for repeated start conditions. * Enqueues writing the given 7 bit address, followed by one bit indicating
*/ * whether this is a read or write request.
I2CTransaction& start(); *
/* Enqueues a stop condition. */ * This command will expect an ACK before continuing.
I2CTransaction& stop(); */
I2CTransaction& write_addr(uint8_t addr, uint8_t op);
/* /*
* Enqueues writing the given 7 bit address, followed by one bit indicating * Enqueues one or more bytes to be written. The transaction will wait for
* whether this is a read or write request. * an ACK to be returned before writing the next byte.
* */
* This command will expect an ACK before continuing. I2CTransaction& write_ack(uint8_t data);
*/ template <typename... More>
I2CTransaction& write_addr(uint8_t addr, uint8_t op); I2CTransaction& write_ack(uint8_t data, More... more) {
write_ack(data);
write_ack(more...);
return *this;
}
/* /*
* Enqueues one or more bytes to be written. The transaction will wait for * Enqueues a read of one byte into the given uint8. Responds with the given
* an ACK to be returned before writing the next byte. * ACK/NACK type.
*/ */
I2CTransaction& write_ack(uint8_t data); I2CTransaction& read(uint8_t* dest, i2c_ack_type_t ack);
template <typename ...More>
I2CTransaction& write_ack(uint8_t data, More... more) {
write_ack(data);
write_ack(more...);
return *this;
}
/* /* Returns the underlying command buffer. */
* Enqueues a read of one byte into the given uint8. Responds with the given i2c_cmd_handle_t handle() { return handle_; }
* ACK/NACK type.
*/
I2CTransaction& read(uint8_t *dest, i2c_ack_type_t ack);
/* Returns the underlying command buffer. */ // Cannot be moved or copied, since doing so is probably an error. Pass a
i2c_cmd_handle_t handle() { return handle_; } // reference instead.
I2CTransaction(const I2CTransaction&) = delete;
I2CTransaction& operator=(const I2CTransaction&) = delete;
// Cannot be moved or copied, since doing so is probably an error. Pass a private:
// reference instead. i2c_cmd_handle_t handle_;
I2CTransaction(const I2CTransaction&) = delete;
I2CTransaction& operator=(const I2CTransaction&) = delete;
private:
i2c_cmd_handle_t handle_;
}; };
} // namespace gay_ipod } // namespace gay_ipod

@ -0,0 +1,224 @@
#include "playback.h"
#include <cstdint>
#include "audio_element.h"
#include "audio_event_iface.h"
#include "audio_pipeline.h"
#include "dac.h"
#include "driver/i2s.h"
#include "esp_err.h"
#include "mp3_decoder.h"
static const char* kTag = "PLAYBACK";
static const i2s_port_t kI2SPort = I2S_NUM_0;
namespace gay_ipod {
// Static functions for interrop with the ESP IDF API, which requires a
// function pointer.
namespace callback {
static DacAudioPlayback* instance = nullptr;
// Fits the required function pointer signature, but just delegates to the
// wrapper function. Does that make this the wrapper? Who knows.
__attribute__((unused)) // (gcc incorrectly thinks this is unused)
static esp_err_t
on_event(audio_event_iface_msg_t* event, void* data) {
if (instance == nullptr) {
ESP_LOGW(kTag, "uncaught ADF event, type %x", event->cmd);
return ESP_OK;
}
return instance->HandleEvent(event, data);
}
} // namespace callback
auto DacAudioPlayback::create(AudioDac* dac)
-> cpp::result<std::unique_ptr<DacAudioPlayback>, Error> {
assert(callback::instance == nullptr);
// Ensure we're soft-muted before initialising, in order to reduce protential
// clicks and pops.
dac->WriteVolume(255);
audio_pipeline_handle_t pipeline;
audio_element_handle_t fatfs_stream_reader;
audio_element_handle_t i2s_stream_writer;
audio_event_iface_handle_t event_interface;
audio_pipeline_cfg_t pipeline_config =
audio_pipeline_cfg_t(DEFAULT_AUDIO_PIPELINE_CONFIG());
pipeline = audio_pipeline_init(&pipeline_config);
if (pipeline == NULL) {
return cpp::fail(Error::PIPELINE_INIT);
}
fatfs_stream_cfg_t fatfs_stream_config =
fatfs_stream_cfg_t(FATFS_STREAM_CFG_DEFAULT());
fatfs_stream_config.type = AUDIO_STREAM_READER;
fatfs_stream_reader = fatfs_stream_init(&fatfs_stream_config);
if (fatfs_stream_reader == NULL) {
return cpp::fail(Error::PIPELINE_INIT);
}
i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{
.type = AUDIO_STREAM_WRITER,
.i2s_config =
{
// static_cast bc esp-adf uses enums incorrectly
.mode = static_cast<i2s_mode_t>(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 44100,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
},
.i2s_port = kI2SPort,
.use_alc = false,
.volume = 0, // Does nothing; use AudioDac to change this.
.out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
.task_stack = I2S_STREAM_TASK_STACK,
.task_core = I2S_STREAM_TASK_CORE,
.task_prio = I2S_STREAM_TASK_PRIO,
.stack_in_ext = false,
.multi_out_num = 0,
.uninstall_drv = true,
.need_expand = false,
.expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
};
i2s_stream_writer = i2s_stream_init(&i2s_stream_config);
if (i2s_stream_writer == NULL) {
return cpp::fail(Error::PIPELINE_INIT);
}
// NOTE: i2s_stream_init does some additional setup that hardcodes MCK as
// GPIO0. This happens to work fine for us, but be careful if changing.
i2s_pin_config_t pin_config = {.mck_io_num = GPIO_NUM_0,
.bck_io_num = GPIO_NUM_26,
.ws_io_num = GPIO_NUM_27,
.data_out_num = GPIO_NUM_5,
.data_in_num = I2S_PIN_NO_CHANGE};
if (esp_err_t err = i2s_set_pin(kI2SPort, &pin_config) != ESP_OK) {
ESP_LOGE(kTag, "failed to configure i2s pins %x", err);
return cpp::fail(Error::PIPELINE_INIT);
}
// TODO: Create encoders dynamically when we need them.
audio_element_handle_t mp3_decoder;
mp3_decoder_cfg_t mp3_config =
mp3_decoder_cfg_t(DEFAULT_MP3_DECODER_CONFIG());
mp3_decoder = mp3_decoder_init(&mp3_config);
assert(mp3_decoder != NULL);
audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG();
event_config.on_cmd = &callback::on_event;
event_interface = audio_event_iface_init(&event_config);
audio_pipeline_set_listener(pipeline, event_interface);
// TODO: most of this is likely post-init, since it involves a decoder.
// All the elements of our pipeline have been initialised. Now switch them
// together.
audio_pipeline_register(pipeline, fatfs_stream_reader, "file");
audio_pipeline_register(pipeline, mp3_decoder, "dec");
audio_pipeline_register(pipeline, i2s_stream_writer, "i2s");
const char* link_tag[3] = {"file", "dec", "i2s"};
audio_pipeline_link(pipeline, &link_tag[0], 3);
return std::make_unique<DacAudioPlayback>(dac, pipeline, fatfs_stream_reader,
i2s_stream_writer, event_interface,
mp3_decoder);
}
DacAudioPlayback::DacAudioPlayback(AudioDac* dac,
audio_pipeline_handle_t pipeline,
audio_element_handle_t fatfs_stream_reader,
audio_element_handle_t i2s_stream_writer,
audio_event_iface_handle_t event_interface,
audio_element_handle_t mp3_decoder)
: dac_(dac),
pipeline_(pipeline),
fatfs_stream_reader_(fatfs_stream_reader),
i2s_stream_writer_(i2s_stream_writer),
event_interface_(event_interface),
mp3_decoder_(mp3_decoder) {
callback::instance = this;
}
DacAudioPlayback::~DacAudioPlayback() {
dac_->WriteVolume(255);
audio_pipeline_remove_listener(pipeline_);
callback::instance = nullptr;
audio_pipeline_stop(pipeline_);
audio_pipeline_wait_for_stop(pipeline_);
audio_pipeline_terminate(pipeline_);
audio_pipeline_unregister(pipeline_, fatfs_stream_reader_);
audio_pipeline_unregister(pipeline_, mp3_decoder_);
audio_pipeline_unregister(pipeline_, i2s_stream_writer_);
audio_event_iface_destroy(event_interface_);
audio_pipeline_deinit(pipeline_);
audio_element_deinit(fatfs_stream_reader_);
audio_element_deinit(i2s_stream_writer_);
audio_element_deinit(mp3_decoder_);
}
void DacAudioPlayback::Play(const std::string& filename) {
dac_->WriteVolume(255);
// TODO: handle reconfiguring the pipeline if needed.
audio_element_set_uri(fatfs_stream_reader_, filename.c_str());
audio_pipeline_run(pipeline_);
dac_->WriteVolume(volume_);
}
void DacAudioPlayback::Resume() {
// TODO.
}
void DacAudioPlayback::Pause() {
// TODO.
}
void DacAudioPlayback::WaitForSongEnd() {
while (1) {
audio_element_state_t state = audio_element_get_state(i2s_stream_writer_);
if (state == AEL_STATE_FINISHED) {
return;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* for gapless */
void DacAudioPlayback::set_next_file(const std::string& filename) {
next_filename_ = filename;
}
void DacAudioPlayback::set_volume(uint8_t volume) {
volume_ = volume;
// TODO: don't write immediately if we're muting to change track or similar.
dac_->WriteVolume(volume);
}
auto DacAudioPlayback::volume() -> uint8_t {
return volume_;
}
auto DacAudioPlayback::HandleEvent(audio_event_iface_msg_t* event, void* data)
-> esp_err_t {
ESP_LOGI(kTag, "got event!");
return ESP_OK;
}
} // namespace gay_ipod

@ -0,0 +1,68 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "audio_common.h"
#include "audio_element.h"
#include "audio_event_iface.h"
#include "audio_pipeline.h"
#include "dac.h"
#include "esp_err.h"
#include "fatfs_stream.h"
#include "i2s_stream.h"
#include "result.hpp"
#include "storage.h"
namespace gay_ipod {
class DacAudioPlayback {
public:
enum Error { PIPELINE_INIT };
static auto create(AudioDac* dac)
-> cpp::result<std::unique_ptr<DacAudioPlayback>, Error>;
DacAudioPlayback(AudioDac* dac,
audio_pipeline_handle_t pipeline,
audio_element_handle_t fatfs_stream_reader,
audio_element_handle_t i2s_stream_writer,
audio_event_iface_handle_t event_interface,
audio_element_handle_t mp3_decoder);
~DacAudioPlayback();
void Play(const std::string& filename);
void Resume();
void Pause();
// For debug :)
void WaitForSongEnd();
/* for gapless */
void set_next_file(const std::string& filename);
void set_volume(uint8_t volume);
auto volume() -> uint8_t;
auto HandleEvent(audio_event_iface_msg_t* event, void* data) -> esp_err_t;
// Not copyable or movable.
DacAudioPlayback(const DacAudioPlayback&) = delete;
DacAudioPlayback& operator=(const DacAudioPlayback&) = delete;
private:
AudioDac* dac_;
std::mutex playback_lock_;
std::string next_filename_;
uint8_t volume_;
audio_pipeline_handle_t pipeline_;
audio_element_handle_t fatfs_stream_reader_;
audio_element_handle_t i2s_stream_writer_;
audio_event_iface_handle_t event_interface_;
audio_element_handle_t mp3_decoder_;
};
} // namespace gay_ipod

@ -1,5 +1,6 @@
#include "storage.h" #include "storage.h"
#include <atomic>
#include <mutex> #include <mutex>
#include "diskio_impl.h" #include "diskio_impl.h"
@ -15,77 +16,130 @@
#include "hal/spi_types.h" #include "hal/spi_types.h"
#include "sdmmc_cmd.h" #include "sdmmc_cmd.h"
static const char* kTag = "SDSTORAGE";
static const uint8_t kMaxOpenFiles = 8;
namespace gay_ipod { namespace gay_ipod {
static const char* TAG = "SDSTORAGE"; const char* kStoragePath = "/sdcard";
SdStorage::SdStorage(GpioExpander *gpio) { // Static functions for interrop with the ESP IDF API, which requires a
this->gpio_ = gpio; // function pointer.
} namespace callback {
static std::atomic<SdStorage*> instance = nullptr;
static std::atomic<esp_err_t (*)(sdspi_dev_handle_t, sdmmc_command_t*)>
bootstrap = nullptr;
SdStorage::~SdStorage() {} static esp_err_t do_transaction(sdspi_dev_handle_t handle,
sdmmc_command_t* cmdinfo) {
auto bootstrap_fn = bootstrap.load();
if (bootstrap_fn != nullptr) {
return bootstrap_fn(handle, cmdinfo);
}
auto instance_unwrapped = instance.load();
if (instance_unwrapped == nullptr) {
ESP_LOGW(kTag, "uncaught sdspi transaction");
return ESP_OK;
}
// TODO: what if a transaction comes in right now?
return instance_unwrapped->HandleTransaction(handle, cmdinfo);
}
} // namespace callback
SdStorage::Error SdStorage::Acquire(void) { auto SdStorage::create(GpioExpander* gpio)
-> cpp::result<std::unique_ptr<SdStorage>, Error> {
// Acquiring the bus will also flush the mux switch change. // Acquiring the bus will also flush the mux switch change.
gpio_->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP);
sdspi_dev_handle_t handle;
sdmmc_host_t host;
sdmmc_card_t card;
FATFS* fs = nullptr;
// Now we can init the driver and set up the SD card into SPI mode. // Now we can init the driver and set up the SD card into SPI mode.
sdspi_host_init(); sdspi_host_init();
sdspi_device_config_t config = { sdspi_device_config_t config = {
.host_id = VSPI_HOST, .host_id = VSPI_HOST,
// CS handled manually bc it's on the GPIO expander // CS handled manually bc it's on the GPIO expander
.gpio_cs = GPIO_NUM_2, .gpio_cs = GPIO_NUM_2,
.gpio_cd = SDSPI_SLOT_NO_CD, .gpio_cd = SDSPI_SLOT_NO_CD,
.gpio_wp = SDSPI_SLOT_NO_WP, .gpio_wp = SDSPI_SLOT_NO_WP,
.gpio_int = GPIO_NUM_NC, .gpio_int = GPIO_NUM_NC,
}; };
ESP_ERROR_CHECK(sdspi_host_init_device(&config, &handle_)); if (esp_err_t err = sdspi_host_init_device(&config, &handle) != ESP_OK) {
ESP_LOGE(kTag, "Failed to init, err %d", err);
return cpp::fail(Error::FAILED_TO_INIT);
}
host_ = sdmmc_host_t SDSPI_HOST_DEFAULT(); host = sdmmc_host_t SDSPI_HOST_DEFAULT();
// We manage the CS pin ourselves via the GPIO expander. To do this safely in // We manage the CS pin ourselves via the GPIO expander. To do this safely in
// a multithreaded environment, we wrap the ESP IDF do_transaction function // a multithreaded environment, we wrap the ESP IDF do_transaction function
// with our own that acquires the CS mutex for the duration of the SPI // with our own that acquires the CS mutex for the duration of the SPI
// transaction. // transaction.
auto src = host_.do_transaction; auto do_transaction = host.do_transaction;
sdspi::do_transaction_wrapper = [=](sdspi_dev_handle_t handle, sdmmc_command_t *cmd) -> esp_err_t { host.do_transaction = &callback::do_transaction;
auto lock = gpio_->AcquireSpiBus(GpioExpander::SD_CARD); host.slot = handle;
return src(handle, cmd); callback::bootstrap = do_transaction;
};
host_.do_transaction = &sdspi::do_transaction;
host_.slot = handle_; auto lock = gpio->AcquireSpiBus(GpioExpander::SD_CARD);
// Will return ESP_ERR_INVALID_RESPONSE if there is no card // Will return ESP_ERR_INVALID_RESPONSE if there is no card
esp_err_t err = sdmmc_card_init(&host_, &card_); esp_err_t err = sdmmc_card_init(&host, &card);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to read, err: %d", err); ESP_LOGW(kTag, "Failed to read, err: %d", err);
return Error::FAILED_TO_READ; return cpp::fail(Error::FAILED_TO_READ);
} }
ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs_)); ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs));
ff_diskio_register_sdmmc(fs_->pdrv, &card_); ff_diskio_register_sdmmc(fs->pdrv, &card);
// Mount right now, not on first operation. // Mount right now, not on first operation.
FRESULT ferr = f_mount(fs_, "", 1); FRESULT ferr = f_mount(fs, "", 1);
if (ferr != FR_OK) { if (ferr != FR_OK) {
ESP_LOGW(TAG, "Failed to mount, err: %d", ferr); ESP_LOGW(kTag, "Failed to mount, err: %d", ferr);
return Error::FAILED_TO_MOUNT; return cpp::fail(Error::FAILED_TO_MOUNT);
} }
return Error::OK; return std::make_unique<SdStorage>(gpio, do_transaction, handle, host, card,
fs);
}
SdStorage::SdStorage(GpioExpander* gpio,
esp_err_t (*do_transaction)(sdspi_dev_handle_t,
sdmmc_command_t*),
sdspi_dev_handle_t handle,
sdmmc_host_t host,
sdmmc_card_t card,
FATFS* fs)
: gpio_(gpio),
do_transaction_(do_transaction),
handle_(handle),
host_(host),
card_(card),
fs_(fs) {
callback::instance = this;
callback::bootstrap = nullptr;
} }
void SdStorage::Release(void) { SdStorage::~SdStorage() {
// Unmount and unregister the filesystem // Unmount and unregister the filesystem
f_unmount(""); f_unmount("");
ff_diskio_register(fs_->pdrv, NULL); ff_diskio_register(fs_->pdrv, NULL);
esp_vfs_fat_unregister_path(kStoragePath); esp_vfs_fat_unregister_path(kStoragePath);
fs_ = nullptr; fs_ = nullptr;
callback::instance = nullptr;
// Uninstall the SPI driver // Uninstall the SPI driver
sdspi_host_remove_device(this->handle_); sdspi_host_remove_device(this->handle_);
sdspi_host_deinit(); sdspi_host_deinit();
} }
} // namespace gay_ipod auto SdStorage::HandleTransaction(sdspi_dev_handle_t handle,
sdmmc_command_t* cmdinfo) -> esp_err_t {
auto lock = gpio_->AcquireSpiBus(GpioExpander::SD_CARD);
return do_transaction_(handle, cmdinfo);
}
} // namespace gay_ipod

@ -1,73 +1,59 @@
#pragma once #pragma once
#include <memory>
#include "driver/sdmmc_types.h" #include "driver/sdmmc_types.h"
#include "driver/sdspi_host.h" #include "driver/sdspi_host.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_vfs_fat.h" #include "esp_vfs_fat.h"
#include "gpio-expander.h" #include "gpio-expander.h"
#include "result.hpp"
namespace gay_ipod { namespace gay_ipod {
// Static functions for interrop with the ESP IDF API, which requires a function extern const char* kStoragePath;
// pointer.
namespace sdspi {
// Holds a lambda created by SdStorage.
static std::function<esp_err_t(sdspi_dev_handle_t,sdmmc_command_t*)> do_transaction_wrapper;
// Fits the required function pointer signature, but just delegates to the
// wrapper function. Does that make this the wrapper? Who knows.
__attribute__ ((unused)) // (gcc incorrectly thinks this is unused)
static esp_err_t do_transaction(sdspi_dev_handle_t handle, sdmmc_command_t *cmdinfo) {
return do_transaction_wrapper(handle, cmdinfo);
}
} // namespace sdspi
static const char *kStoragePath = "/sdcard";
static const uint8_t kMaxOpenFiles = 8;
class SdStorage { class SdStorage {
public: public:
SdStorage(GpioExpander *gpio); enum Error {
~SdStorage(); FAILED_TO_INIT,
/** We couldn't interact with the SD card at all. Is it missing? */
enum Error { FAILED_TO_READ,
OK, /** We couldn't mount the SD card. Is it formatted? */
/** We couldn't interact with the SD card at all. Is it missing? */ FAILED_TO_MOUNT,
FAILED_TO_READ, };
/** We couldn't mount the SD card. Is it formatted? */
FAILED_TO_MOUNT, static auto create(GpioExpander* gpio)
}; -> cpp::result<std::unique_ptr<SdStorage>, Error>;
// FIXME: these methods should also handling powering the SD card up and SdStorage(GpioExpander* gpio,
// down once we have that capability. esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*),
sdspi_dev_handle_t handle_,
/** sdmmc_host_t host_,
* Initialises the SDSPI driver and mounts the SD card for reading and sdmmc_card_t card_,
* writing. This must be called before any interactions with the underlying FATFS* fs_);
* storage. ~SdStorage();
*/
Error Acquire(void); auto HandleTransaction(sdspi_dev_handle_t handle, sdmmc_command_t* cmdinfo)
-> esp_err_t;
/**
* Unmounts the SD card and frees memory associated with the SDSPI driver. // Not copyable or movable.
*/ // TODO: maybe this could be movable?
void Release(void); SdStorage(const SdStorage&) = delete;
SdStorage& operator=(const SdStorage&) = delete;
// Not copyable or movable.
// TODO: maybe this could be movable? private:
SdStorage(const SdStorage&) = delete; GpioExpander* gpio_;
SdStorage& operator=(const SdStorage&) = delete;
esp_err_t (*do_transaction_)(sdspi_dev_handle_t, sdmmc_command_t*) = nullptr;
private:
GpioExpander *gpio_; // SPI and SD driver info
sdspi_dev_handle_t handle_;
// SPI and SD driver info sdmmc_host_t host_;
sdspi_dev_handle_t handle_; sdmmc_card_t card_;
sdmmc_host_t host_;
sdmmc_card_t card_; // Filesystem info
FATFS* fs_ = nullptr;
// Filesystem info
FATFS *fs_ = nullptr;
}; };
} // namespace gay_ipod } // namespace gay_ipod

Loading…
Cancel
Save