diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b51c3259..2601a809 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register( - SRCS "gay-ipod-fw.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" + SRCS "gay-ipod-fw.cpp" "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" INCLUDE_DIRS "." REQUIRES "esp_adc_cal" "fatfs") diff --git a/main/battery.h b/main/battery.h index 0ee36aa5..4b7bece0 100644 --- a/main/battery.h +++ b/main/battery.h @@ -1,5 +1,4 @@ -#ifndef BATTERY_H -#define BATTERY_H +#pragma once #include @@ -15,5 +14,3 @@ esp_err_t init_adc(void); uint32_t read_battery_voltage(void); } // namespace gay_ipod - -#endif diff --git a/main/dac.cpp b/main/dac.cpp new file mode 100644 index 00000000..625c924a --- /dev/null +++ b/main/dac.cpp @@ -0,0 +1,220 @@ +#include "dac.h" + +#include "esp_log.h" +#include "assert.h" +#include "driver/i2c.h" +#include "gpio-expander.h" +#include "hal/i2c_types.h" +#include + +namespace gay_ipod { + +static const char* TAG = "AUDIODAC"; + +/** + * Utility method for writing to a register on the PCM5122. Writes two bytes: + * first the address for the register that we're writing to, and then the value + * to write. + * + * Note this function assumes that the correct page has already been selected. + */ +static void set_register(i2c_cmd_handle_t handle, uint8_t reg, uint8_t val) { + i2c_master_write_byte(handle, reg, true); + i2c_master_write_byte(handle, val, true); +} + +AudioDac::AudioDac(GpioExpander *gpio) { + this->gpio_ = gpio; +}; + +AudioDac::~AudioDac() {}; + +void AudioDac::Start(SampleRate sample_rate, BitDepth bit_depth) { + // First check that the arguments look okay. + + // Check that the bit clock (PLL input) is between 1MHz and 50MHz + uint32_t bckFreq = sample_rate * bit_depth * 2; + if (bckFreq < 1000000 || bckFreq > 50000000) { + return; + } + + // 24 bits is not supported for 44.1kHz and 48kHz. + if ((sample_rate == SAMPLE_RATE_44_1K || sample_rate == SAMPLE_RATE_48K) + && bit_depth == BIT_DEPTH_24) { + return; + } + + // Now do all the math required to correctly set up the internal clocks. + int p, j, d, r; + int nmac, ndac, ncp, dosr, idac; + if (sample_rate == SAMPLE_RATE_11_025K || + sample_rate == SAMPLE_RATE_22_05K || + sample_rate == SAMPLE_RATE_44_1K) { + //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 (bit_depth == BIT_DEPTH_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 (sample_rate) { + case SAMPLE_RATE_16K: nmac = 6; break; + case SAMPLE_RATE_32K: nmac = 3; break; + default: nmac = 2; break; + }; + + ndac = 16; + ncp = 4; + dosr = 384000 / sample_rate; + idac = 98304000 / nmac / sample_rate; // DSP clock / sample rate + } + + // FS speed mode + int speedMode; + if (sample_rate <= SAMPLE_RATE_48K) { + speedMode = 0; + } else if (sample_rate <= SAMPLE_RATE_96K) { + speedMode = 1; + } else if (sample_rate <= SAMPLE_RATE_192K) { + speedMode = 2; + } else { + speedMode = 3; + } + + // Set correct I2S config + uint8_t i2s_format = 0; + switch (bit_depth) { + case BIT_DEPTH_16: i2s_format = 0x00; break; + case BIT_DEPTH_24: i2s_format = 0x02; break; + case BIT_DEPTH_32: i2s_format = 0x03; break; + }; + + // We've calculated all of our data. Now assemble the big list of registers + // that we need to configure. + i2c_cmd_handle_t handle = i2c_cmd_link_create(); + if (handle == NULL) { + return; + } + + i2c_master_start(handle); + i2c_master_write_byte(handle, (kPCM5122Address << 1 | I2C_MASTER_WRITE), true); + + // All our registers are on the first page. + set_register(handle, Register::PAGE_SELECT, 0); + + // Disable clock autoset and ignore SCK detection + set_register(handle, Register::IGNORE_ERRORS, 0x1A); + // Set PLL clock source to BCK + set_register(handle, Register::PLL_CLOCK_SOURCE, 0x10); + // Set DAC clock source to PLL output + set_register(handle, Register::DAC_CLOCK_SOURCE, 0x10); + + // Configure PLL + set_register(handle, Register::PLL_P, p - 1); + set_register(handle, Register::PLL_J, j); + set_register(handle, Register::PLL_D_MSB, (d >> 8) & 0x3F); + set_register(handle, Register::PLL_D_LSB, d & 0xFF); + set_register(handle, Register::PLL_R, r - 1); + + // Clock dividers + set_register(handle, Register::DSP_CLOCK_DIV, nmac - 1); + set_register(handle, Register::DAC_CLOCK_DIV, ndac - 1); + set_register(handle, Register::NCP_CLOCK_DIV, ncp - 1); + set_register(handle, Register::OSR_CLOCK_DIV, dosr - 1); + + // IDAC (nb of DSP clock cycles per sample) + set_register(handle, Register::IDAC_MSB, (idac >> 8) & 0xFF); + set_register(handle, Register::IDAC_LSB, idac & 0xFF); + + set_register(handle, Register::FS_SPEED_MODE, speedMode); + set_register(handle, Register::I2S_FORMAT, i2s_format); + + i2c_master_stop(handle); + + // TODO: Handle this gracefully. + ESP_ERROR_CHECK(i2c_master_cmd_begin(I2C_NUM_0, handle, 50)); + + i2c_cmd_link_delete(handle); + + vTaskDelay(pdMS_TO_TICKS(10)); + + // TODO: Handle this gracefully. + assert(ReadPowerState() == 0x05); +} + +uint8_t AudioDac::ReadPowerState() { + i2c_cmd_handle_t handle = i2c_cmd_link_create(); + if (handle == NULL) { + return 0; + } + + uint8_t result = 0; + + i2c_master_start(handle); + i2c_master_write_byte(handle, (kPCM5122Address << 1 | I2C_MASTER_WRITE), true); + i2c_master_write_byte(handle, DSP_BOOT_POWER_STATE, true); + i2c_master_start(handle); + i2c_master_read_byte(handle, &result, I2C_MASTER_NACK); + i2c_master_stop(handle); + + ESP_ERROR_CHECK(i2c_master_cmd_begin(I2C_NUM_0, handle, 50)); + + i2c_cmd_link_delete(handle); + + return result; +} + +void AudioDac::WriteVolume(uint8_t volume) { + i2c_cmd_handle_t handle = i2c_cmd_link_create(); + if (handle == NULL) { + return; + } + + i2c_master_start(handle); + i2c_master_write_byte(handle, (kPCM5122Address << 1 | I2C_MASTER_WRITE), true); + + set_register(handle, Register::DIGITAL_VOLUME_L, volume); + set_register(handle, Register::DIGITAL_VOLUME_R, volume); + + i2c_master_stop(handle); + + i2c_master_cmd_begin(I2C_NUM_0, handle, 50); + + i2c_cmd_link_delete(handle); +} + +void AudioDac::WritePowerMode(PowerMode mode) { + switch (mode) { + case ON: + case STANDBY: + // TODO: enable power switch. + break; + case OFF: + // TODO: disable power switch. + break; + } +} + +} // namespace gay_ipod diff --git a/main/dac.h b/main/dac.h new file mode 100644 index 00000000..f196b315 --- /dev/null +++ b/main/dac.h @@ -0,0 +1,96 @@ +#pragma once + +#include "gpio-expander.h" +#include + +namespace gay_ipod { + + static const uint8_t kPCM5122Address = 0x4C; + +/** + * PCM5122PWR + * + * Heavily inspired from the Arduino library here: + * https://github.com/tommag/PCM51xx_Arduino/ + */ +class AudioDac { + public: + AudioDac(GpioExpander *gpio); + ~AudioDac(); + + /** Supported sample rates */ + enum SampleRate { + SAMPLE_RATE_8K = 8000, + SAMPLE_RATE_11_025K = 11025, + SAMPLE_RATE_16K = 16000, + SAMPLE_RATE_22_05K = 22050, + SAMPLE_RATE_32K = 32000, + SAMPLE_RATE_44_1K = 44100, + SAMPLE_RATE_48K = 48000, + SAMPLE_RATE_96K = 96000, + SAMPLE_RATE_192K = 192000, + SAMPLE_RATE_384K = 384000 + }; + + enum BitDepth { + BIT_DEPTH_16 = 16, + BIT_DEPTH_24 = 24, + BIT_DEPTH_32 = 32, + }; + + /** + * Performs initial configuration of the DAC and sets it to begin expecting + * I2S audio data. + */ + void Start(SampleRate sample_rate, BitDepth bit_depth); + + /** + * 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 PowerMode { + ON = 0, + STANDBY = 0x10, + OFF = 0x01, + }; + + void WritePowerMode(PowerMode state); + + // Not copyable or movable. + // TODO: maybe this could be movable? + AudioDac(const AudioDac&) = delete; + AudioDac& operator=(const AudioDac&) = delete; + + private: + GpioExpander *gpio_; + PowerMode last_power_state_ = PowerMode::OFF; + + enum Register { + PAGE_SELECT = 0, + IGNORE_ERRORS = 37, + PLL_CLOCK_SOURCE = 13, + DAC_CLOCK_SOURCE = 14, + PLL_P = 20, + PLL_J = 21, + PLL_D_MSB = 22, + PLL_D_LSB = 23, + PLL_R = 24, + DSP_CLOCK_DIV = 27, + DAC_CLOCK_DIV = 14, + NCP_CLOCK_DIV = 29, + OSR_CLOCK_DIV = 30, + IDAC_MSB = 35, + IDAC_LSB = 36, + FS_SPEED_MODE = 34, + I2S_FORMAT = 40, + DIGITAL_VOLUME_L = 61, + DIGITAL_VOLUME_R = 62, + DSP_BOOT_POWER_STATE = 118, + }; + + uint8_t ReadPowerState(); +}; + +} // namespace gay_ipod diff --git a/main/gay-ipod-fw.cpp b/main/gay-ipod-fw.cpp index 84a5b007..3b6c855f 100644 --- a/main/gay-ipod-fw.cpp +++ b/main/gay-ipod-fw.cpp @@ -2,6 +2,7 @@ #include #include "battery.h" +#include "dac.h" #include "driver/adc.h" #include "driver/gpio.h" #include "driver/i2c.h" @@ -101,17 +102,28 @@ extern "C" void app_main(void) ESP_LOGI(TAG, "Init ADC"); ESP_ERROR_CHECK(gay_ipod::init_adc()); - ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); - vTaskDelay(pdMS_TO_TICKS(2500)); - - ESP_LOGI(TAG, "Trying to init SD card"); + ESP_LOGI(TAG, "Init SD card"); gay_ipod::SdStorage storage(&expander); - gay_ipod::SdStorage::Error err = storage.Acquire(); if (err != gay_ipod::SdStorage::Error::OK) { ESP_LOGE(TAG, "Failed to acquire storage!"); return; } + + ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); + vTaskDelay(pdMS_TO_TICKS(1500)); + + /* + * TODO: not working :( + ESP_LOGI(TAG, "Trying to init DAC"); + gay_ipod::AudioDac dac(&expander); + dac.Start( + gay_ipod::AudioDac::SAMPLE_RATE_44_1K, + gay_ipod::AudioDac::BIT_DEPTH_16); + + vTaskDelay(pdMS_TO_TICKS(1000)); + */ + ESP_LOGI(TAG, "Looks okay? Let's list some files!"); vTaskDelay(pdMS_TO_TICKS(1000)); diff --git a/main/gpio-expander.cpp b/main/gpio-expander.cpp index d17f9f07..aedfbf2b 100644 --- a/main/gpio-expander.cpp +++ b/main/gpio-expander.cpp @@ -48,16 +48,16 @@ esp_err_t GpioExpander::Read() { return ret; } -bool GpioExpander::charge_power_ok(void) { +bool GpioExpander::charge_power_ok(void) const { // Active-low. return (input_a_ & (1 << 4)) == 0; } -bool GpioExpander::headphone_detect(void) { +bool GpioExpander::headphone_detect(void) const { return (input_b_ & (1 << 0)); } -uint8_t GpioExpander::key_states(void) { +uint8_t GpioExpander::key_states(void) const { return input_b_ & 0b00111111; } diff --git a/main/gpio-expander.h b/main/gpio-expander.h index ec34a0f1..2b213ddc 100644 --- a/main/gpio-expander.h +++ b/main/gpio-expander.h @@ -1,5 +1,4 @@ -#ifndef GPIO_EXPANDER_H -#define GPIO_EXPANDER_H +#pragma once #include @@ -23,9 +22,9 @@ class GpioExpander { esp_err_t Write(void); esp_err_t Read(void); - bool charge_power_ok(void); - bool headphone_detect(void); - uint8_t key_states(void); + bool charge_power_ok(void) const; + bool headphone_detect(void) const; + uint8_t key_states(void) const; enum SdMuxController { ESP, @@ -71,5 +70,3 @@ class GpioExpander { }; } // namespace gay_ipod - -#endif diff --git a/main/storage.h b/main/storage.h index cffeb3fc..5db3685f 100644 --- a/main/storage.h +++ b/main/storage.h @@ -1,5 +1,4 @@ -#ifndef STORAGE_H -#define STORAGE_H +#pragma once #include "driver/sdmmc_types.h" #include "driver/sdspi_host.h" @@ -58,5 +57,3 @@ class SdStorage { }; } // namespace gay_ipod - -#endif