From 28d73ad8660e27f9c7b20b6e978d3d0c412dec00 Mon Sep 17 00:00:00 2001 From: jacqueline Date: Mon, 7 Nov 2022 12:01:29 +1100 Subject: Split driver-y things into a separate component --- CMakeLists.txt | 13 +- main/CMakeLists.txt | 5 - main/battery.cpp | 31 ---- main/battery.hpp | 16 -- main/dac.cpp | 106 ------------- main/dac.hpp | 73 --------- main/display-init.cpp | 103 ------------ main/display-init.hpp | 81 ---------- main/display.cpp | 285 ---------------------------------- main/display.hpp | 67 -------- main/gay-ipod-fw.cpp | 201 ------------------------ main/gpio-expander.cpp | 87 ----------- main/gpio-expander.hpp | 208 ------------------------- main/i2c.cpp | 51 ------ main/i2c.hpp | 84 ---------- main/playback.cpp | 246 ----------------------------- main/playback.hpp | 67 -------- main/storage.cpp | 149 ------------------ main/storage.hpp | 60 ------- src/drivers/CMakeLists.txt | 5 + src/drivers/battery.cpp | 31 ++++ src/drivers/dac.cpp | 106 +++++++++++++ src/drivers/display-init.cpp | 103 ++++++++++++ src/drivers/display.cpp | 285 ++++++++++++++++++++++++++++++++++ src/drivers/gpio-expander.cpp | 87 +++++++++++ src/drivers/i2c.cpp | 51 ++++++ src/drivers/include/battery.hpp | 16 ++ src/drivers/include/dac.hpp | 73 +++++++++ src/drivers/include/display-init.hpp | 81 ++++++++++ src/drivers/include/display.hpp | 67 ++++++++ src/drivers/include/gpio-expander.hpp | 208 +++++++++++++++++++++++++ src/drivers/include/i2c.hpp | 84 ++++++++++ src/drivers/include/playback.hpp | 67 ++++++++ src/drivers/include/storage.hpp | 60 +++++++ src/drivers/playback.cpp | 246 +++++++++++++++++++++++++++++ src/drivers/storage.cpp | 149 ++++++++++++++++++ src/main/CMakeLists.txt | 3 + src/main/main.cpp | 201 ++++++++++++++++++++++++ 38 files changed, 1934 insertions(+), 1922 deletions(-) delete mode 100644 main/CMakeLists.txt delete mode 100644 main/battery.cpp delete mode 100644 main/battery.hpp delete mode 100644 main/dac.cpp delete mode 100644 main/dac.hpp delete mode 100644 main/display-init.cpp delete mode 100644 main/display-init.hpp delete mode 100644 main/display.cpp delete mode 100644 main/display.hpp delete mode 100644 main/gay-ipod-fw.cpp delete mode 100644 main/gpio-expander.cpp delete mode 100644 main/gpio-expander.hpp delete mode 100644 main/i2c.cpp delete mode 100644 main/i2c.hpp delete mode 100644 main/playback.cpp delete mode 100644 main/playback.hpp delete mode 100644 main/storage.cpp delete mode 100644 main/storage.hpp create mode 100644 src/drivers/CMakeLists.txt create mode 100644 src/drivers/battery.cpp create mode 100644 src/drivers/dac.cpp create mode 100644 src/drivers/display-init.cpp create mode 100644 src/drivers/display.cpp create mode 100644 src/drivers/gpio-expander.cpp create mode 100644 src/drivers/i2c.cpp create mode 100644 src/drivers/include/battery.hpp create mode 100644 src/drivers/include/dac.hpp create mode 100644 src/drivers/include/display-init.hpp create mode 100644 src/drivers/include/display.hpp create mode 100644 src/drivers/include/gpio-expander.hpp create mode 100644 src/drivers/include/i2c.hpp create mode 100644 src/drivers/include/playback.hpp create mode 100644 src/drivers/include/storage.hpp create mode 100644 src/drivers/playback.cpp create mode 100644 src/drivers/storage.cpp create mode 100644 src/main/CMakeLists.txt create mode 100644 src/main/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e5f8cb98..93dc6e89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,25 @@ # For more information about build system see # https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html -# The following five lines of boilerplate have to be in your project's -# CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.8) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include($ENV{ADF_PATH}/CMakeLists.txt) + +# Build only the subset of components that we actually depend on. +set(COMPONENTS "") + +# External dependencies list(APPEND EXTRA_COMPONENT_DIRS "lib/result") list(APPEND EXTRA_COMPONENT_DIRS "lib/lvgl") +# Project components +list(APPEND EXTRA_COMPONENT_DIRS "src/") + include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(gay-ipod-fw) +# Extra build flags that should apply to the entire build. This should mostly +# just be used to setting flags that our external dependencies requires. +# Otherwise, prefer adding per-component build flags to keep things neat. idf_build_set_property(COMPILE_OPTIONS "-DRESULT_DISABLE_EXCEPTIONS -DLV_CONF_INCLUDE_SIMPLE" APPEND) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt deleted file mode 100644 index 5ef0bf9e..00000000 --- a/main/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -idf_component_register( - SRCS "gay-ipod-fw.cpp" "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" - "i2c.cpp" "playback.cpp" "display.cpp" "display-init.cpp" - INCLUDE_DIRS "." - REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" "lvgl") diff --git a/main/battery.cpp b/main/battery.cpp deleted file mode 100644 index 66c96daf..00000000 --- a/main/battery.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "battery.hpp" - -#include "driver/adc.h" -#include "esp_adc_cal.h" -#include "hal/adc_types.h" - -namespace gay_ipod { - -static esp_adc_cal_characteristics_t calibration; - -esp_err_t init_adc(void) { - // Calibration should already be fused into the chip from the factory, so - // we should only need to read it back out again. - esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, - &calibration); - - // 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. - adc1_config_width(ADC_WIDTH_BIT_12); - adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); - - return ESP_OK; -} - -uint32_t read_battery_voltage(void) { - // GPIO 34 - int raw = adc1_get_raw(ADC1_CHANNEL_6); - return esp_adc_cal_raw_to_voltage(raw, &calibration); -} - -} // namespace gay_ipod diff --git a/main/battery.hpp b/main/battery.hpp deleted file mode 100644 index 399e866f..00000000 --- a/main/battery.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -#include "esp_err.h" - -namespace gay_ipod { - -esp_err_t init_adc(void); - -/** - * Returns the current battery level in millivolts. - */ -uint32_t read_battery_voltage(void); - -} // namespace gay_ipod diff --git a/main/dac.cpp b/main/dac.cpp deleted file mode 100644 index d74e9523..00000000 --- a/main/dac.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include "dac.hpp" - -#include "gpio-expander.hpp" -#include "i2c.hpp" - -#include - -#include "assert.h" -#include "driver/i2c.h" -#include "esp_err.h" -#include "esp_log.h" -#include "hal/i2c_types.h" - -namespace gay_ipod { - -static const char* kTag = "AUDIODAC"; -static const uint8_t kPcm5122Address = 0x4C; -static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS; - -auto AudioDac::create(GpioExpander* expander) - -> cpp::result, Error> { - std::unique_ptr dac = std::make_unique(expander); - - 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); - } - - dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4); - dac->WriteVolume(100); - - bool is_configured = - 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 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) { - WriteRegister(Register::DIGITAL_VOLUME_L, volume); - WriteRegister(Register::DIGITAL_VOLUME_R, volume); -} - -std::pair AudioDac::ReadPowerState() { - uint8_t result = 0; - - I2CTransaction transaction; - transaction.start() - .write_addr(kPcm5122Address, I2C_MASTER_WRITE) - .write_ack(DSP_BOOT_POWER_STATE) - .start() - .write_addr(kPcm5122Address, I2C_MASTER_READ) - .read(&result, I2C_MASTER_NACK) - .stop(); - - ESP_ERROR_CHECK(transaction.Execute()); - - bool is_booted = result >> 7; - PowerState detail = (PowerState)(result & 0b1111); - return std::pair(is_booted, detail); -} - -bool AudioDac::WaitForPowerState( - std::function predicate) { - bool has_matched = false; - for (int i = 0; i < 10; i++) { - std::pair result = ReadPowerState(); - has_matched = predicate(result.first, result.second); - if (has_matched) { - break; - } else { - ESP_LOGI(kTag, "Waiting for power state (was %d %x)", result.first, - (uint8_t)result.second); - vTaskDelay(pdMS_TO_TICKS(1)); - } - } - return has_matched; -} - -void AudioDac::WriteRegister(Register reg, uint8_t val) { - I2CTransaction transaction; - transaction.start() - .write_addr(kPcm5122Address, I2C_MASTER_WRITE) - .write_ack(reg, val) - .stop(); - // TODO: Retry once? - ESP_ERROR_CHECK(transaction.Execute()); -} - -} // namespace gay_ipod diff --git a/main/dac.hpp b/main/dac.hpp deleted file mode 100644 index 6d025384..00000000 --- a/main/dac.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include "gpio-expander.hpp" - -#include -#include - -#include "esp_err.h" -#include "result.hpp" - -namespace gay_ipod { - -/** - * Interface for a PCM5122PWR DAC, configured over I2C. - */ -class AudioDac { - public: - enum Error { - FAILED_TO_BOOT, - FAILED_TO_CONFIGURE, - }; - static auto create(GpioExpander* expander) - -> cpp::result, Error>; - - AudioDac(GpioExpander* gpio); - ~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 ReadPowerState(); - - // Not copyable or movable. - AudioDac(const AudioDac&) = delete; - AudioDac& operator=(const AudioDac&) = delete; - - private: - GpioExpander* gpio_; - - /* - * Pools the power state for up to 10ms, waiting for the given predicate to - * be true. - */ - bool WaitForPowerState(std::function predicate); - - enum Register { - PAGE_SELECT = 0, - DE_EMPHASIS = 7, - DIGITAL_VOLUME_L = 61, - DIGITAL_VOLUME_R = 62, - DSP_BOOT_POWER_STATE = 118, - }; - - void WriteRegister(Register reg, uint8_t val); -}; - -} // namespace gay_ipod diff --git a/main/display-init.cpp b/main/display-init.cpp deleted file mode 100644 index e4545339..00000000 --- a/main/display-init.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "display-init.hpp" - -namespace gay_ipod { -namespace displays { - -/* Bit to use to signify we should delay after part of an init sequence */ -const uint8_t kDelayBit = 0x80; - -// ST7735 commands and general format from the Adafruit library for these -// displays. AFAICT it's the most complete implementation out there, and I -// really don't want to have to derive this from the datasheet myself. -// See https://github.com/adafruit/Adafruit-ST7735-Library/ - -// clang-format off -static const uint8_t kST7735RCommonHeader[]{ - 15, // 15 commands in list: - ST77XX_SWRESET, kDelayBit, // 1: Software reset, 0 args, w/delay - 150, // 150 ms delay - ST77XX_SLPOUT, kDelayBit, // 2: Out of sleep mode, 0 args, w/delay - 255, // 500 ms delay - ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: - 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) - ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: - 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) - ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: - 0x01, 0x2C, 0x2D, // Dot inversion mode - 0x01, 0x2C, 0x2D, // Line inversion mode - ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: - 0x07, // No inversion - ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: - 0xA2, - 0x02, // -4.6V - 0x84, // AUTO mode - ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: - 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD - ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: - 0x0A, // Opamp current small - 0x00, // Boost frequency - ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: - 0x8A, // BCLK/2, - 0x2A, // opamp current small & medium low - ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: - 0x8A, 0xEE, - ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: - 0x0E, - ST77XX_INVOFF, 0, // 13: Don't invert display, no args - ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: - 0xC8, // row/col addr, bottom-top refresh - ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: - 0x05 -}; - -// Commands to include for the variant of the panel that has a green pull tab on -// the screen protector. -static const uint8_t kST7735RCommonGreen[]{ - 2, // 2 commands in list: - ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: - 0x00, 0x02, // XSTART = 0 - 0x00, 0x7F+0x02, // XEND = 127 - ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: - 0x00, 0x01, // XSTART = 0 - 0x00, 0x9F+0x01}; - -// Commands to include for the variant of the panel that has a red pull tab on -// the screen protector. -static const uint8_t kST7735RCommonRed[]{ - 3, // 2 commands in list: - ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: - 0x00, 0x00, // XSTART = 0 - 0x00, 0x7F, // XEND = 127 - ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: - 0x00, 0x00, // XSTART = 0 - 0x00, 0x9F, - ST77XX_MADCTL, 1, - 0xC0, -}; - -static const uint8_t kST7735RCommonFooter[]{ - 4, // 4 commands in list: - ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: - 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides - 0x37, 0x32, 0x29, 0x2d, // accurate colors) - 0x29, 0x25, 0x2B, 0x39, - 0x00, 0x01, 0x03, 0x10, - ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: - 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides - 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) - 0x2E, 0x2E, 0x37, 0x3F, - 0x00, 0x00, 0x02, 0x10, - ST77XX_NORON, kDelayBit, // 3: Normal display on, no args, w/delay - 10, // 10 ms delay - ST77XX_DISPON, kDelayBit, // 4: Main screen turn on, no args w/delay - 100 -}; -// clang-format on - -const InitialisationData kST7735R = { - .num_sequences = 3, - .sequences = {kST7735RCommonHeader, kST7735RCommonRed, - kST7735RCommonFooter}}; - -} // namespace displays -} // namespace gay_ipod diff --git a/main/display-init.hpp b/main/display-init.hpp deleted file mode 100644 index f11e9b57..00000000 --- a/main/display-init.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include - -namespace gay_ipod { -namespace displays { - -extern const uint8_t kDelayBit; - -struct InitialisationData { - uint8_t num_sequences; - const uint8_t* sequences[4]; -}; - -extern const InitialisationData kST7735R; - -/* - * Valid command bytes that can be sent to ST77XX displays, as well as commands - * for more specific variants. - */ -enum StCommands { - ST77XX_NOP = 0x00, - ST77XX_SWRESET = 0x01, - ST77XX_RDDID = 0x04, - ST77XX_RDDST = 0x09, - - ST77XX_SLPIN = 0x10, - ST77XX_SLPOUT = 0x11, - ST77XX_PTLON = 0x12, - ST77XX_NORON = 0x13, - - ST77XX_INVOFF = 0x20, - ST77XX_INVON = 0x21, - ST77XX_DISPOFF = 0x28, - ST77XX_DISPON = 0x29, - ST77XX_CASET = 0x2A, - ST77XX_RASET = 0x2B, - ST77XX_RAMWR = 0x2C, - ST77XX_RAMRD = 0x2E, - - ST77XX_PTLAR = 0x30, - ST77XX_TEOFF = 0x34, - ST77XX_TEON = 0x35, - ST77XX_MADCTL = 0x36, - ST77XX_COLMOD = 0x3A, - - ST77XX_MADCTL_MY = 0x80, - ST77XX_MADCTL_MX = 0x40, - ST77XX_MADCTL_MV = 0x20, - ST77XX_MADCTL_ML = 0x10, - ST77XX_MADCTL_RGB = 0x00, - - ST77XX_RDID1 = 0xDA, - ST77XX_RDID2 = 0xDB, - ST77XX_RDID3 = 0xDC, - ST77XX_RDID4 = 0xDD, - - ST7735_MADCTL_BGR = 0x08, - ST7735_MADCTL_MH = 0x04, - - ST7735_FRMCTR1 = 0xB1, - ST7735_FRMCTR2 = 0xB2, - ST7735_FRMCTR3 = 0xB3, - ST7735_INVCTR = 0xB4, - ST7735_DISSET5 = 0xB6, - - ST7735_PWCTR1 = 0xC0, - ST7735_PWCTR2 = 0xC1, - ST7735_PWCTR3 = 0xC2, - ST7735_PWCTR4 = 0xC3, - ST7735_PWCTR5 = 0xC4, - ST7735_VMCTR1 = 0xC5, - - ST7735_PWCTR6 = 0xFC, - - ST7735_GMCTRP1 = 0xE0, - ST7735_GMCTRN1 = 0xE1, -}; - -} // namespace displays -} // namespace gay_ipod diff --git a/main/display.cpp b/main/display.cpp deleted file mode 100644 index 8708436f..00000000 --- a/main/display.cpp +++ /dev/null @@ -1,285 +0,0 @@ -#include "display.hpp" -#include -#include -#include -#include -#include -#include "assert.h" -#include "display-init.hpp" -#include "driver/gpio.h" -#include "driver/spi_master.h" -#include "esp_attr.h" -#include "esp_heap_caps.h" -#include "freertos/portable.h" -#include "freertos/portmacro.h" -#include "freertos/projdefs.h" -#include "hal/gpio_types.h" -#include "hal/spi_types.h" -#include "lvgl/lvgl.h" - -static const char* kTag = "DISPLAY"; -static const gpio_num_t kCommandOrDataPin = GPIO_NUM_21; -static const gpio_num_t kLedPin = GPIO_NUM_22; - -static const uint8_t kDisplayWidth = 128; -static const uint8_t kDisplayHeight = 160; -static const uint8_t kTransactionQueueSize = 10; - -/* - * The size of each of our two display buffers. This is fundamentally a balance - * between performance and memory usage. LVGL docs recommend a buffer 1/10th the - * size of the screen is the best tradeoff. - * We use two buffers so that one can be flushed to the screen at the same time - * as the other is being drawn. - */ -static const int kDisplayBufferSize = (kDisplayWidth * kDisplayHeight) / 10; - -// Allocate both buffers in static memory to ensure that they're in DRAM, with -// minimal fragmentation. We most cases we always need these buffers anyway, so -// it's not a memory hit we can avoid anyway. -// Note: 128 * 160 / 10 * 2 bpp * 2 buffers = 8 KiB -DMA_ATTR static lv_color_t sBuffer1[kDisplayBufferSize]; -DMA_ATTR static lv_color_t sBuffer2[kDisplayBufferSize]; - -namespace gay_ipod { - -// Static functions for interrop with the LVGL display driver API, which -// requires a function pointer. -namespace callback { -static std::atomic instance = nullptr; - -static void flush_cb(lv_disp_drv_t* disp_drv, - const lv_area_t* area, - lv_color_t* color_map) { - auto instance_unwrapped = instance.load(); - if (instance_unwrapped == nullptr) { - ESP_LOGW(kTag, "uncaught flush callback"); - return; - } - // TODO: what if a transaction comes in right now? - instance_unwrapped->Flush(disp_drv, area, color_map); -} - -static void IRAM_ATTR post_cb(spi_transaction_t* transaction) { - auto instance_unwrapped = instance.load(); - if (instance_unwrapped == nullptr) { - // Can't log in ISR. - return; - } - instance_unwrapped->PostTransaction(*transaction); -} -} // namespace callback - -auto Display::create(GpioExpander* expander, - const displays::InitialisationData& init_data) - -> cpp::result, Error> { - // First, set up our GPIOs - gpio_config_t gpio_cfg = { - .pin_bit_mask = GPIO_SEL_22 | GPIO_SEL_21, - .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - gpio_config(&gpio_cfg); - - gpio_set_level(kLedPin, 1); - gpio_set_level(kCommandOrDataPin, 0); - - // Next, init the SPI device - spi_device_interface_config_t spi_cfg = { - .command_bits = 0, // No command phase - .address_bits = 0, // No address phase - .dummy_bits = 0, - // For ST7789, mode should be 2 - .mode = 0, - .duty_cycle_pos = 0, // Unused - .cs_ena_pretrans = 0, - .cs_ena_posttrans = 0, - .clock_speed_hz = SPI_MASTER_FREQ_40M, - .input_delay_ns = 0, - .spics_io_num = -1, // TODO: change for R2 - .flags = 0, - .queue_size = kTransactionQueueSize, - .pre_cb = NULL, - .post_cb = &callback::post_cb, - }; - spi_device_handle_t handle; - spi_bus_add_device(VSPI_HOST, &spi_cfg, &handle); - - // TODO: ideally create this later? a bit awkward rn. - auto display = std::make_unique(expander, handle); - - // Now we reset the display into a known state, then configure it - // TODO: set rotatoin - ESP_LOGI(kTag, "Sending init sequences"); - for (int i = 0; i < init_data.num_sequences; i++) { - display->SendInitialisationSequence(init_data.sequences[i]); - } - - // The hardware is now configured correctly. Next, initialise the LVGL display - // driver. - ESP_LOGI(kTag, "Init buffers"); - lv_disp_draw_buf_init(&display->buffers_, sBuffer1, sBuffer2, - kDisplayBufferSize); - lv_disp_drv_init(&display->driver_); - display->driver_.draw_buf = &display->buffers_; - display->driver_.hor_res = kDisplayWidth; - display->driver_.ver_res = kDisplayHeight; - display->driver_.flush_cb = &callback::flush_cb; - - ESP_LOGI(kTag, "Registering driver"); - display->display_ = lv_disp_drv_register(&display->driver_); - - return std::move(display); -} - -Display::Display(GpioExpander* gpio, spi_device_handle_t handle) - : gpio_(gpio), handle_(handle) { - callback::instance = this; -} - -Display::~Display() { - callback::instance = nullptr; - // TODO. -} - -void Display::SendInitialisationSequence(const uint8_t* data) { - uint8_t command, num_args; - uint16_t sleep_duration_ms; - - // First byte of the data is the number of commands. - for (int i = *(data++); i > 0; i--) { - command = *(data++); - num_args = *(data++); - bool has_delay = (num_args & displays::kDelayBit) > 0; - num_args &= ~displays::kDelayBit; - - SendCommandWithData(command, data, num_args); - - data += num_args; - if (has_delay) { - sleep_duration_ms = *(data++); - if (sleep_duration_ms == 0xFF) { - sleep_duration_ms = 500; - } - vTaskDelay(pdMS_TO_TICKS(sleep_duration_ms)); - } - } -} - -void Display::SendCommandWithData(uint8_t command, - const uint8_t* data, - size_t length, - uintptr_t flags) { - SendCmd(&command, 1, flags); - SendData(data, length, flags); -} - -void Display::SendCmd(const uint8_t* data, size_t length, uintptr_t flags) { - SendTransaction(COMMAND, data, length, flags); -} - -void Display::SendData(const uint8_t* data, size_t length, uintptr_t flags) { - SendTransaction(DATA, data, length, flags); -} - -void Display::SendTransaction(TransactionType type, - const uint8_t* data, - size_t length, - uint32_t flags) { - if (length == 0) { - return; - } - - // TODO: Use a memory pool for these. - spi_transaction_t* transaction = (spi_transaction_t*)heap_caps_calloc( - 1, sizeof(spi_transaction_t), MALLOC_CAP_DMA); - - transaction->rx_buffer = NULL; - // Length is in bits, so multiply by 8. - transaction->length = length * 8; - transaction->rxlength = 0; // Match `length` value. - - // If the data to transmit is very short, then we can fit it directly - // inside the transaction struct. - if (length * 8 <= 32) { - transaction->flags = SPI_TRANS_USE_TXDATA; - std::memcpy(&transaction->tx_data, data, length); - } else { - // TODO: copy data to a DMA-capable transaction buffer - transaction->tx_buffer = const_cast(data); - } - - transaction->user = reinterpret_cast(flags); - - // TODO: acquire the bus first? Or in an outer scope? - // TODO: fail gracefully - // ESP_ERROR_CHECK(spi_device_queue_trans(handle_, transaction, - // portMAX_DELAY)); - // - - ServiceTransactions(); - gpio_set_level(kCommandOrDataPin, type); - - gpio_->with([&](auto& gpio_) { - gpio_.set_pin(GpioExpander::DISPLAY_CHIP_SELECT, 0); - }); - { - // auto lock = gpio_->AcquireSpiBus(GpioExpander::DISPLAY); - ESP_ERROR_CHECK(spi_device_polling_transmit(handle_, transaction)); - } - - free(transaction); -} - -void Display::Flush(lv_disp_drv_t* disp_drv, - const lv_area_t* area, - lv_color_t* color_map) { - uint16_t data[2] = {0, 0}; - - data[0] = SPI_SWAP_DATA_TX(area->x1, 16); - data[1] = SPI_SWAP_DATA_TX(area->x2, 16); - SendCommandWithData(displays::ST77XX_CASET, (uint8_t*)data, 4); - - data[0] = SPI_SWAP_DATA_TX(area->y1, 16); - data[1] = SPI_SWAP_DATA_TX(area->y2, 16); - SendCommandWithData(displays::ST77XX_RASET, (uint8_t*)data, 4); - - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - SendCommandWithData(displays::ST77XX_RAMWR, (uint8_t*)color_map, size * 2, - LVGL_FLUSH); - - // ESP_LOGI(kTag, "finished flush."); - // lv_disp_flush_ready(&driver_); -} - -void IRAM_ATTR Display::PostTransaction(const spi_transaction_t& transaction) { - if (reinterpret_cast(transaction.user) & LVGL_FLUSH) { - lv_disp_flush_ready(&driver_); - } -} - -void Display::ServiceTransactions() { - // todo - if (1) - return; - spi_transaction_t* transaction = nullptr; - // TODO: just wait '1' here, provide mechanism to wait for sure (poll?) - while (spi_device_get_trans_result(handle_, &transaction, pdMS_TO_TICKS(1)) != - ESP_ERR_TIMEOUT) { - ESP_LOGI(kTag, "cleaning up finished transaction"); - - // TODO: a bit dodge lmao - // TODO: also this should happen in the post callback instead i guess? - if (transaction->length > 1000) { - ESP_LOGI(kTag, "finished flush."); - lv_disp_flush_ready(&driver_); - } - - // TODO: place back into pool. - free(transaction); - } -} - -} // namespace gay_ipod diff --git a/main/display.hpp b/main/display.hpp deleted file mode 100644 index 2d6e9cd6..00000000 --- a/main/display.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include "display-init.hpp" -#include "driver/spi_master.h" -#include "gpio-expander.hpp" -#include "lvgl/lvgl.h" -#include "result.hpp" - -namespace gay_ipod { - -/* - * Display driver for LVGL. - */ -class Display { - public: - enum Error {}; - static auto create(GpioExpander* expander, - const displays::InitialisationData& init_data) - -> cpp::result, Error>; - - Display(GpioExpander* gpio, spi_device_handle_t handle); - ~Display(); - - void WriteData(); - - void Flush(lv_disp_drv_t* disp_drv, - const lv_area_t* area, - lv_color_t* color_map); - - void IRAM_ATTR PostTransaction(const spi_transaction_t& transaction); - - void ServiceTransactions(); - - private: - GpioExpander* gpio_; - spi_device_handle_t handle_; - - lv_disp_draw_buf_t buffers_; - lv_disp_drv_t driver_; - lv_disp_t* display_ = nullptr; - - enum TransactionType { - COMMAND = 0, - DATA = 1, - }; - - enum TransactionFlags { - LVGL_FLUSH = 1, - }; - - void SendInitialisationSequence(const uint8_t* data); - - void SendCommandWithData(uint8_t command, - const uint8_t* data, - size_t length, - uintptr_t flags = 0); - - void SendCmd(const uint8_t* data, size_t length, uintptr_t flags = 0); - void SendData(const uint8_t* data, size_t length, uintptr_t flags = 0); - void SendTransaction(TransactionType type, - const uint8_t* data, - size_t length, - uintptr_t flags = 0); -}; - -} // namespace gay_ipod diff --git a/main/gay-ipod-fw.cpp b/main/gay-ipod-fw.cpp deleted file mode 100644 index 8d989146..00000000 --- a/main/gay-ipod-fw.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include "battery.hpp" -#include "core/lv_disp.h" -#include "core/lv_obj_pos.h" -#include "dac.hpp" -#include "display-init.hpp" -#include "display.hpp" -#include "esp_freertos_hooks.h" -#include "freertos/portmacro.h" -#include "gpio-expander.hpp" -#include "misc/lv_color.h" -#include "misc/lv_timer.h" -#include "playback.hpp" -#include "storage.hpp" - -#include -#include -#include -#include -#include - -#include "audio_common.h" -#include "audio_element.h" -#include "audio_pipeline.h" -#include "driver/gpio.h" -#include "driver/i2c.h" -#include "driver/sdspi_host.h" -#include "driver/spi_common.h" -#include "driver/spi_master.h" -#include "esp_intr_alloc.h" -#include "esp_log.h" -#include "hal/gpio_types.h" -#include "hal/spi_types.h" -#include "lvgl/lvgl.h" -#include "widgets/lv_label.h" - -#define I2C_SDA_IO (GPIO_NUM_2) -#define I2C_SCL_IO (GPIO_NUM_4) -#define I2C_CLOCK_HZ (400000) - -#define SPI_SDI_IO (GPIO_NUM_19) -#define SPI_SDO_IO (GPIO_NUM_23) -#define SPI_SCLK_IO (GPIO_NUM_18) -#define SPI_QUADWP_IO (GPIO_NUM_22) -#define SPI_QUADHD_IO (GPIO_NUM_21) - -static const char* TAG = "MAIN"; - -esp_err_t init_i2c(void) { - i2c_port_t port = I2C_NUM_0; - i2c_config_t config = { - .mode = I2C_MODE_MASTER, - .sda_io_num = I2C_SDA_IO, - .scl_io_num = I2C_SCL_IO, - .sda_pullup_en = GPIO_PULLUP_ENABLE, - .scl_pullup_en = GPIO_PULLUP_ENABLE, - .master = - { - .clk_speed = I2C_CLOCK_HZ, - }, - // No requirements for the clock. - .clk_flags = 0, - }; - - ESP_ERROR_CHECK(i2c_param_config(port, &config)); - ESP_ERROR_CHECK(i2c_driver_install(port, config.mode, 0, 0, 0)); - - // TODO: INT line - - return ESP_OK; -} - -esp_err_t init_spi(void) { - spi_bus_config_t config = { - .mosi_io_num = SPI_SDO_IO, - .miso_io_num = SPI_SDI_IO, - .sclk_io_num = SPI_SCLK_IO, - .quadwp_io_num = -1, // SPI_QUADWP_IO, - .quadhd_io_num = -1, // SPI_QUADHD_IO, - - // Unused - .data4_io_num = -1, - .data5_io_num = -1, - .data6_io_num = -1, - .data7_io_num = -1, - - // Use the DMA default size. The display requires larger buffers, but it - // manages its down use of DMA-capable memory. - .max_transfer_sz = 128 * 16 * 2, // TODO: hmm - .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, - .intr_flags = 0, - }; - - ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &config, SPI_DMA_CH_AUTO)); - - return ESP_OK; -} - -void IRAM_ATTR tick_hook(void) { - lv_tick_inc(1); -} - -static const size_t kLvglStackSize = 8 * 1024; -static StaticTask_t sLvglTaskBuffer = {}; -static StackType_t sLvglStack[kLvglStackSize] = {0}; - -struct LvglArgs { - gay_ipod::GpioExpander* gpio_expander; -}; - -void lvgl_main(void* voidArgs) { - ESP_LOGI(TAG, "starting LVGL task"); - LvglArgs* args = (LvglArgs*)voidArgs; - gay_ipod::GpioExpander* gpio_expander = args->gpio_expander; - - // Dispose of the args now that we've gotten everything out of them. - delete args; - - ESP_LOGI(TAG, "init lvgl"); - lv_init(); - - // LVGL has been initialised, so we can now start reporting ticks to it. - esp_register_freertos_tick_hook(&tick_hook); - - ESP_LOGI(TAG, "init display"); - auto display_res = - gay_ipod::Display::create(gpio_expander, gay_ipod::displays::kST7735R); - if (display_res.has_error()) { - ESP_LOGE(TAG, "Failed: %d", display_res.error()); - return; - } - std::unique_ptr display = std::move(display_res.value()); - - auto label = lv_label_create(NULL); - lv_label_set_text(label, "g'day, cunts!"); - lv_obj_center(label); - lv_scr_load(label); - - while (1) { - lv_timer_handler(); - // display->ServiceTransactions(); - vTaskDelay(pdMS_TO_TICKS(10)); - } - - // TODO: break from the loop to kill this task, so that we can do our RAII - // cleanup, unregister our tick callback and so on. -} - -extern "C" void app_main(void) { - ESP_LOGI(TAG, "Initialising peripherals"); - - ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED)); - init_i2c(); - init_spi(); - ESP_ERROR_CHECK(gay_ipod::init_adc()); - - ESP_LOGI(TAG, "Init GPIOs"); - gay_ipod::GpioExpander* expander = new gay_ipod::GpioExpander(); - - // for debugging usb ic - // expander.set_sd_mux(gay_ipod::GpioExpander::USB); - - /* - ESP_LOGI(TAG, "Init SD card"); - auto storage_res = gay_ipod::SdStorage::create(expander); - if (storage_res.has_error()) { - ESP_LOGE(TAG, "Failed: %d", storage_res.error()); - return; - } - std::unique_ptr storage = std::move(storage_res.value()); - - ESP_LOGI(TAG, "Init DAC"); - auto dac_res = gay_ipod::AudioDac::create(expander); - if (storage_res.has_error()) { - ESP_LOGE(TAG, "Failed: %d", dac_res.error()); - return; - } - std::unique_ptr dac = std::move(dac_res.value()); - - ESP_LOGI(TAG, "Init Audio Pipeline"); - auto playback_res = gay_ipod::DacAudioPlayback::create(dac.get()); - if (playback_res.has_error()) { - ESP_LOGE(TAG, "Failed: %d", playback_res.error()); - return; - } - std::unique_ptr playback = - std::move(playback_res.value()); - */ - - ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); - vTaskDelay(pdMS_TO_TICKS(1500)); - - LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs)); - lvglArgs->gpio_expander = expander; - xTaskCreateStaticPinnedToCore(&lvgl_main, "LVGL", kLvglStackSize, (void*)lvglArgs, - 1, sLvglStack, &sLvglTaskBuffer, 1); - - while (1) { - // TODO: Find owners for everything so we can quit this task safely. - vTaskDelay(pdMS_TO_TICKS(1000)); - } -} diff --git a/main/gpio-expander.cpp b/main/gpio-expander.cpp deleted file mode 100644 index 6b472d1c..00000000 --- a/main/gpio-expander.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "gpio-expander.hpp" - -#include "i2c.hpp" - -#include - -namespace gay_ipod { - -GpioExpander::GpioExpander() { - ports_ = pack(kPortADefault, kPortBDefault); - // Read and write initial values on initialisation so that we do not have a - // strange partially-initialised state. - // TODO: log or abort if these error; it's really bad! - Write(); - Read(); -} - -GpioExpander::~GpioExpander() {} - -void GpioExpander::with(std::function f) { - f(*this); - Write(); -} - -esp_err_t GpioExpander::Write() { - i2c_cmd_handle_t handle = i2c_cmd_link_create(); - if (handle == NULL) { - return ESP_ERR_NO_MEM; - } - - std::pair ports_ab = unpack(ports()); - - I2CTransaction transaction; - transaction.start() - .write_addr(kPca8575Address, I2C_MASTER_WRITE) - .write_ack(ports_ab.first, ports_ab.second) - .stop(); - - return transaction.Execute(); -} - -esp_err_t GpioExpander::Read() { - uint8_t input_a, input_b; - - I2CTransaction transaction; - transaction.start() - .write_addr(kPca8575Address, I2C_MASTER_READ) - .read(&input_a, I2C_MASTER_ACK) - .read(&input_b, I2C_MASTER_LAST_NACK) - .stop(); - - esp_err_t ret = transaction.Execute(); - inputs_ = pack(input_a, input_b); - return ret; -} - -void GpioExpander::set_pin(ChipSelect cs, bool value) { - set_pin((Pin)cs, value); -} - -void GpioExpander::set_pin(Pin pin, bool value) { - if (value) { - ports_ |= (1 << pin); - } else { - ports_ &= ~(1 << pin); - } -} - -bool GpioExpander::get_input(Pin pin) const { - return (inputs_ & (1 << pin)) > 0; -} - -GpioExpander::SpiLock GpioExpander::AcquireSpiBus(ChipSelect cs) { - // TODO: also spi_device_acquire_bus? - return SpiLock(*this, cs); -} - -GpioExpander::SpiLock::SpiLock(GpioExpander& gpio, ChipSelect cs) - : lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) { - gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 0); }); -} - -GpioExpander::SpiLock::~SpiLock() { - gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 1); }); -} - -} // namespace gay_ipod diff --git a/main/gpio-expander.hpp b/main/gpio-expander.hpp deleted file mode 100644 index 2a12fba4..00000000 --- a/main/gpio-expander.hpp +++ /dev/null @@ -1,208 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "driver/i2c.h" -#include "esp_check.h" -#include "esp_err.h" -#include "esp_log.h" -#include "freertos/FreeRTOS.h" - -namespace gay_ipod { - -/** - * Wrapper for interfacing with the PCA8575 GPIO expander. Includes basic - * low-level pin setting methods, as well as higher level convenience functions - * for reading, writing, and atomically interacting with the SPI chip select - * pins. - * - * Each method of this class can be called safely from any thread, and all - * updates are guaranteed to be atomic. Any access to chip select related pins - * should be done whilst holding `cs_lock` (preferably via the helper methods). - */ -class GpioExpander { - public: - GpioExpander(); - ~GpioExpander(); - - static const uint8_t kPca8575Address = 0x20; - static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS; - - // Port A: - // 0 - audio power enable - // 1 - usb interface power enable - // 2 - display power enable - // 3 - sd card power enable - // 4 - charge power ok (active low) - // 5 - sd mux switch - // 6 - sd chip select - // 7 - display chip select - // All power switches low, chip selects high, active-low charge power high - static const uint8_t kPortADefault = 0b11010001; - - // Port B: - // 0 - 3.5mm jack detect (active low) - // 1 - dac soft mute switch - // 2 - GPIO - // 3 - GPIO - // 4 - GPIO - // 5 - GPIO - // 6 - GPIO - // 7 - GPIO - // DAC mute output low, everything else is active-low inputs. - static const uint8_t kPortBDefault = 0b11111111; - - /* - * Convenience mehod for packing the port a and b bytes into a single 16 bit - * value. - */ - static uint16_t pack(uint8_t a, uint8_t b) { return ((uint16_t)b) << 8 | a; } - - /* - * Convenience mehod for unpacking the result of `pack` back into two single - * byte port datas. - */ - static std::pair 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: - * - * ``` - * gpio_.with([&](auto& gpio) { - * gpio.set_pin(AUDIO_POWER_ENABLE, true); - * }); - * ``` - */ - void with(std::function f); - - /** - * Sets the ports on the GPIO expander to the values currently represented - * in `ports`. - */ - esp_err_t Write(void); - - /** - * Reads from the GPIO expander, populating `inputs` with the most recent - * values. - */ - esp_err_t Read(void); - - /* Maps each pin of the expander to its number in a `pack`ed uint16. */ - enum Pin { - // Port A - AUDIO_POWER_ENABLE = 0, - USB_INTERFACE_POWER_ENABLE = 1, - DISPLAY_POWER_ENABLE = 2, - SD_CARD_POWER_ENABLE = 3, - CHARGE_POWER_OK = 4, // Active-low input - SD_MUX_SWITCH = 5, - SD_CHIP_SELECT = 6, - DISPLAY_CHIP_SELECT = 7, - - // Port B - PHONE_DETECT = 8, // Active-high input - DAC_MUTE = 9, - GPIO_1 = 10, - GPIO_2 = 11, - GPIO_3 = 12, - GPIO_4 = 13, - GPIO_5 = 14, - GPIO_6 = 15, - }; - - /* Pins whose access should be guarded by `cs_lock`. */ - enum ChipSelect { - SD_CARD = SD_CHIP_SELECT, - DISPLAY = DISPLAY_CHIP_SELECT, - }; - - /* Nicer value names for use with the SD_MUX_SWITCH pin. */ - enum SdController { - 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. - */ - std::atomic& ports() { return ports_; } - - /* - * 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. - */ - 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. - */ - const std::atomic& 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 - * `ports()`. - */ - 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_; } - - /* - * Helper class containing an active `cs_mutex` lock. When an instance of - * 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: - SpiLock(GpioExpander& gpio, ChipSelect cs); - ~SpiLock(); - - SpiLock(const SpiLock&) = delete; - - private: - std::scoped_lock lock_; - GpioExpander& gpio_; - ChipSelect cs_; - }; - - /* - * 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 - * recommended way to safely interact with devices on the SPI bus is to have - * a self-contained block like so: - * - * ``` - * { - * auto lock = AcquireSpiBus(WHATEVER); - * // Do some cool things here. - * } - * ``` - */ - SpiLock AcquireSpiBus(ChipSelect cs); - - // Not copyable or movable. There should usually only ever be once instance - // of this class, and that instance will likely have a static lifetime. - GpioExpander(const GpioExpander&) = delete; - GpioExpander& operator=(const GpioExpander&) = delete; - - private: - std::mutex cs_mutex_; - std::atomic ports_; - std::atomic inputs_; -}; - -} // namespace gay_ipod diff --git a/main/i2c.cpp b/main/i2c.cpp deleted file mode 100644 index d3bfaa59..00000000 --- a/main/i2c.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "i2c.hpp" -#include - -#include "assert.h" -#include "driver/i2c.h" - -namespace gay_ipod { - -static constexpr int kCmdLinkSize = I2C_LINK_RECOMMENDED_SIZE(12); - -I2CTransaction::I2CTransaction() { - // Use a fixed size buffer to avoid many many tiny allocations. - buffer_ = (uint8_t*)calloc(sizeof(uint8_t), kCmdLinkSize); - handle_ = i2c_cmd_link_create_static(buffer_, kCmdLinkSize); - assert(handle_ != NULL && "failed to create command link"); -} - -I2CTransaction::~I2CTransaction() { - free(buffer_); -} - -esp_err_t I2CTransaction::Execute() { - return i2c_master_cmd_begin(I2C_NUM_0, handle_, kI2CTimeout); -} - -I2CTransaction& I2CTransaction::start() { - ESP_ERROR_CHECK(i2c_master_start(handle_)); - return *this; -} - -I2CTransaction& I2CTransaction::stop() { - ESP_ERROR_CHECK(i2c_master_stop(handle_)); - return *this; -} - -I2CTransaction& I2CTransaction::write_addr(uint8_t addr, uint8_t op) { - write_ack(addr << 1 | op); - return *this; -} - -I2CTransaction& I2CTransaction::write_ack(uint8_t data) { - ESP_ERROR_CHECK(i2c_master_write_byte(handle_, data, true)); - return *this; -} - -I2CTransaction& I2CTransaction::read(uint8_t* dest, i2c_ack_type_t ack) { - ESP_ERROR_CHECK(i2c_master_read_byte(handle_, dest, ack)); - return *this; -} - -} // namespace gay_ipod diff --git a/main/i2c.hpp b/main/i2c.hpp deleted file mode 100644 index db554f5d..00000000 --- a/main/i2c.hpp +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once - -#include - -#include "driver/i2c.h" -#include "hal/i2c_types.h" - -namespace gay_ipod { - -/* - * Convenience wrapper for performing an I2C transaction with a reasonable - * preconfigured timeout, automatic management of a heap-based command buffer, - * and a terser API for enqueuing bytes. - * - * Any error codes from the underlying ESP IDF are treated as fatal, since they - * typically represent invalid arguments or OOMs. - */ -class I2CTransaction { - public: - static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS; - - 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 doesn’t 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(); - - /* - * Enqueues a start condition. May also be used for repeated start - * conditions. - */ - I2CTransaction& start(); - /* Enqueues a stop condition. */ - I2CTransaction& stop(); - - /* - * Enqueues writing the given 7 bit address, followed by one bit indicating - * whether this is a read or write request. - * - * This command will expect an ACK before continuing. - */ - I2CTransaction& write_addr(uint8_t addr, uint8_t op); - - /* - * Enqueues one or more bytes to be written. The transaction will wait for - * an ACK to be returned before writing the next byte. - */ - I2CTransaction& write_ack(uint8_t data); - template - I2CTransaction& write_ack(uint8_t data, More... more) { - write_ack(data); - write_ack(more...); - return *this; - } - - /* - * Enqueues a read of one byte into the given uint8. Responds with the given - * ACK/NACK type. - */ - I2CTransaction& read(uint8_t* dest, i2c_ack_type_t ack); - - /* Returns the underlying command buffer. */ - i2c_cmd_handle_t handle() { return handle_; } - - // Cannot be moved or copied, since doing so is probably an error. Pass a - // reference instead. - I2CTransaction(const I2CTransaction&) = delete; - I2CTransaction& operator=(const I2CTransaction&) = delete; - - private: - i2c_cmd_handle_t handle_; - uint8_t* buffer_; -}; - -} // namespace gay_ipod diff --git a/main/playback.cpp b/main/playback.cpp deleted file mode 100644 index 46dec680..00000000 --- a/main/playback.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include "playback.hpp" - -#include "dac.hpp" - -#include - -#include "audio_element.h" -#include "audio_event_iface.h" -#include "audio_pipeline.h" -#include "driver/i2s.h" -#include "esp_err.h" -#include "freertos/portmacro.h" -#include "mp3_decoder.h" - -static const char* kTag = "PLAYBACK"; -static const i2s_port_t kI2SPort = I2S_NUM_0; - -namespace gay_ipod { - -static audio_element_status_t status_from_the_void(void* status) { - uintptr_t as_pointer_int = reinterpret_cast(status); - return static_cast(as_pointer_int); -} - -auto DacAudioPlayback::create(AudioDac* dac) - -> cpp::result, Error> { - // 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_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_interface = audio_event_iface_init(&event_config); - - audio_pipeline_set_listener(pipeline, event_interface); - audio_element_msg_set_listener(fatfs_stream_reader, event_interface); - audio_element_msg_set_listener(mp3_decoder, event_interface); - audio_element_msg_set_listener(i2s_stream_writer, 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(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) {} - -DacAudioPlayback::~DacAudioPlayback() { - dac_->WriteVolume(255); - - audio_pipeline_remove_listener(pipeline_); - audio_element_msg_remove_listener(fatfs_stream_reader_, event_interface_); - audio_element_msg_remove_listener(mp3_decoder_, event_interface_); - audio_element_msg_remove_listener(i2s_stream_writer_, event_interface_); - - 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::ProcessEvents() { - while (1) { - audio_event_iface_msg_t event; - esp_err_t err = - audio_event_iface_listen(event_interface_, &event, portMAX_DELAY); - if (err != ESP_OK) { - ESP_LOGI(kTag, "error listening for event:%x", err); - continue; - } - ESP_LOGI(kTag, "received event, cmd %i", event.cmd); - - if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && - event.source == (void*)mp3_decoder_ && - event.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { - audio_element_info_t music_info = {0}; - audio_element_getinfo(mp3_decoder_, &music_info); - ESP_LOGI(kTag, "sample_rate=%d, bits=%d, ch=%d", music_info.sample_rates, - music_info.bits, music_info.channels); - audio_element_setinfo(i2s_stream_writer_, &music_info); - i2s_stream_set_clk(i2s_stream_writer_, music_info.sample_rates, - music_info.bits, music_info.channels); - } - - if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && - event.source == (void*)fatfs_stream_reader_ && - event.cmd == AEL_MSG_CMD_REPORT_STATUS) { - audio_element_status_t status = status_from_the_void(event.data); - if (status == AEL_STATUS_STATE_FINISHED) { - // TODO: enqueue next track? - } - } - - if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && - event.source == (void*)i2s_stream_writer_ && - event.cmd == AEL_MSG_CMD_REPORT_STATUS) { - audio_element_status_t status = status_from_the_void(event.data); - if (status == AEL_STATUS_STATE_FINISHED) { - // TODO. - return; - } - } - - if (event.need_free_data) { - ESP_LOGI(kTag, "freeing event data"); - free(event.data); - } - } -} - -/* 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_; -} - -} // namespace gay_ipod diff --git a/main/playback.hpp b/main/playback.hpp deleted file mode 100644 index 493dd311..00000000 --- a/main/playback.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "dac.hpp" -#include "storage.hpp" - -#include -#include -#include - -#include "audio_common.h" -#include "audio_element.h" -#include "audio_event_iface.h" -#include "audio_pipeline.h" -#include "esp_err.h" -#include "fatfs_stream.h" -#include "i2s_stream.h" -#include "mp3_decoder.h" -#include "result.hpp" - -namespace gay_ipod { - -class DacAudioPlayback { - public: - enum Error { PIPELINE_INIT }; - static auto create(AudioDac* dac) - -> cpp::result, 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(); - - void ProcessEvents(); - - /* for gapless */ - void set_next_file(const std::string& filename); - - void set_volume(uint8_t volume); - auto volume() -> uint8_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 diff --git a/main/storage.cpp b/main/storage.cpp deleted file mode 100644 index 7fed6ed0..00000000 --- a/main/storage.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "storage.hpp" - -#include "gpio-expander.hpp" - -#include -#include -#include - -#include "diskio_impl.h" -#include "diskio_sdmmc.h" -#include "driver/gpio.h" -#include "driver/sdmmc_types.h" -#include "driver/sdspi_host.h" -#include "esp_check.h" -#include "esp_err.h" -#include "esp_vfs_fat.h" -#include "ff.h" -#include "hal/gpio_types.h" -#include "hal/spi_types.h" -#include "sdmmc_cmd.h" - -static const char* kTag = "SDSTORAGE"; -static const uint8_t kMaxOpenFiles = 8; - -namespace gay_ipod { - -const char* kStoragePath = "/sdcard"; - -// Static functions for interrop with the ESP IDF API, which requires a -// function pointer. -namespace callback { -static std::atomic instance = nullptr; -static std::atomic - bootstrap = nullptr; - -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 - -auto SdStorage::create(GpioExpander* gpio) - -> cpp::result, Error> { - // Acquiring the bus will also flush the mux switch change. - gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); - - sdspi_dev_handle_t handle; - std::unique_ptr host; - std::unique_ptr card; - FATFS* fs = nullptr; - - // Now we can init the driver and set up the SD card into SPI mode. - sdspi_host_init(); - - sdspi_device_config_t config = { - .host_id = VSPI_HOST, - // CS handled manually bc it's on the GPIO expander - .gpio_cs = GPIO_NUM_2, - .gpio_cd = SDSPI_SLOT_NO_CD, - .gpio_wp = SDSPI_SLOT_NO_WP, - .gpio_int = GPIO_NUM_NC, - }; - 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 = std::make_unique(sdmmc_host_t SDSPI_HOST_DEFAULT()); - card = std::make_unique(); - - // 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 - // with our own that acquires the CS mutex for the duration of the SPI - // transaction. - auto do_transaction = host->do_transaction; - host->do_transaction = &callback::do_transaction; - host->slot = handle; - callback::bootstrap = do_transaction; - - auto lock = gpio->AcquireSpiBus(GpioExpander::SD_CARD); - // Will return ESP_ERR_INVALID_RESPONSE if there is no card - esp_err_t err = sdmmc_card_init(host.get(), card.get()); - if (err != ESP_OK) { - ESP_LOGW(kTag, "Failed to read, err: %d", err); - return cpp::fail(Error::FAILED_TO_READ); - } - - ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs)); - ff_diskio_register_sdmmc(fs->pdrv, card.get()); - - // Mount right now, not on first operation. - FRESULT ferr = f_mount(fs, "", 1); - if (ferr != FR_OK) { - ESP_LOGW(kTag, "Failed to mount, err: %d", ferr); - return cpp::fail(Error::FAILED_TO_MOUNT); - } - - return std::make_unique(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, - std::unique_ptr& host, - std::unique_ptr& card, - FATFS* fs) - : gpio_(gpio), - do_transaction_(do_transaction), - handle_(handle), - host_(std::move(host)), - card_(std::move(card)), - fs_(fs) { - callback::instance = this; - callback::bootstrap = nullptr; -} - -SdStorage::~SdStorage() { - // Unmount and unregister the filesystem - f_unmount(""); - ff_diskio_register(fs_->pdrv, NULL); - esp_vfs_fat_unregister_path(kStoragePath); - fs_ = nullptr; - - callback::instance = nullptr; - - // Uninstall the SPI driver - sdspi_host_remove_device(this->handle_); - sdspi_host_deinit(); -} - -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 diff --git a/main/storage.hpp b/main/storage.hpp deleted file mode 100644 index cee49cc5..00000000 --- a/main/storage.hpp +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include "gpio-expander.hpp" - -#include - -#include "driver/sdmmc_types.h" -#include "driver/sdspi_host.h" -#include "esp_err.h" -#include "esp_vfs_fat.h" -#include "result.hpp" - -namespace gay_ipod { - -extern const char* kStoragePath; - -class SdStorage { - public: - enum Error { - FAILED_TO_INIT, - /** We couldn't interact with the SD card at all. Is it missing? */ - FAILED_TO_READ, - /** We couldn't mount the SD card. Is it formatted? */ - FAILED_TO_MOUNT, - }; - - static auto create(GpioExpander* gpio) - -> cpp::result, Error>; - - SdStorage(GpioExpander* gpio, - esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*), - sdspi_dev_handle_t handle_, - std::unique_ptr& host_, - std::unique_ptr& card_, - FATFS* fs_); - ~SdStorage(); - - auto HandleTransaction(sdspi_dev_handle_t handle, sdmmc_command_t* cmdinfo) - -> esp_err_t; - - // Not copyable or movable. - // TODO: maybe this could be movable? - SdStorage(const SdStorage&) = delete; - SdStorage& operator=(const SdStorage&) = delete; - - private: - GpioExpander* gpio_; - - esp_err_t (*do_transaction_)(sdspi_dev_handle_t, sdmmc_command_t*) = nullptr; - - // SPI and SD driver info - sdspi_dev_handle_t handle_; - std::unique_ptr host_; - std::unique_ptr card_; - - // Filesystem info - FATFS* fs_ = nullptr; -}; - -} // namespace gay_ipod diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt new file mode 100644 index 00000000..b9682050 --- /dev/null +++ b/src/drivers/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" + "playback.cpp" "display.cpp" "display-init.cpp" + INCLUDE_DIRS "include" + REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" "lvgl") diff --git a/src/drivers/battery.cpp b/src/drivers/battery.cpp new file mode 100644 index 00000000..66c96daf --- /dev/null +++ b/src/drivers/battery.cpp @@ -0,0 +1,31 @@ +#include "battery.hpp" + +#include "driver/adc.h" +#include "esp_adc_cal.h" +#include "hal/adc_types.h" + +namespace gay_ipod { + +static esp_adc_cal_characteristics_t calibration; + +esp_err_t init_adc(void) { + // Calibration should already be fused into the chip from the factory, so + // we should only need to read it back out again. + esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, + &calibration); + + // 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. + adc1_config_width(ADC_WIDTH_BIT_12); + adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); + + return ESP_OK; +} + +uint32_t read_battery_voltage(void) { + // GPIO 34 + int raw = adc1_get_raw(ADC1_CHANNEL_6); + return esp_adc_cal_raw_to_voltage(raw, &calibration); +} + +} // namespace gay_ipod diff --git a/src/drivers/dac.cpp b/src/drivers/dac.cpp new file mode 100644 index 00000000..d74e9523 --- /dev/null +++ b/src/drivers/dac.cpp @@ -0,0 +1,106 @@ +#include "dac.hpp" + +#include "gpio-expander.hpp" +#include "i2c.hpp" + +#include + +#include "assert.h" +#include "driver/i2c.h" +#include "esp_err.h" +#include "esp_log.h" +#include "hal/i2c_types.h" + +namespace gay_ipod { + +static const char* kTag = "AUDIODAC"; +static const uint8_t kPcm5122Address = 0x4C; +static const uint8_t kPcm5122Timeout = 100 / portTICK_RATE_MS; + +auto AudioDac::create(GpioExpander* expander) + -> cpp::result, Error> { + std::unique_ptr dac = std::make_unique(expander); + + 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); + } + + dac->WriteRegister(Register::DE_EMPHASIS, 1 << 4); + dac->WriteVolume(100); + + bool is_configured = + 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 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) { + WriteRegister(Register::DIGITAL_VOLUME_L, volume); + WriteRegister(Register::DIGITAL_VOLUME_R, volume); +} + +std::pair AudioDac::ReadPowerState() { + uint8_t result = 0; + + I2CTransaction transaction; + transaction.start() + .write_addr(kPcm5122Address, I2C_MASTER_WRITE) + .write_ack(DSP_BOOT_POWER_STATE) + .start() + .write_addr(kPcm5122Address, I2C_MASTER_READ) + .read(&result, I2C_MASTER_NACK) + .stop(); + + ESP_ERROR_CHECK(transaction.Execute()); + + bool is_booted = result >> 7; + PowerState detail = (PowerState)(result & 0b1111); + return std::pair(is_booted, detail); +} + +bool AudioDac::WaitForPowerState( + std::function predicate) { + bool has_matched = false; + for (int i = 0; i < 10; i++) { + std::pair result = ReadPowerState(); + has_matched = predicate(result.first, result.second); + if (has_matched) { + break; + } else { + ESP_LOGI(kTag, "Waiting for power state (was %d %x)", result.first, + (uint8_t)result.second); + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + return has_matched; +} + +void AudioDac::WriteRegister(Register reg, uint8_t val) { + I2CTransaction transaction; + transaction.start() + .write_addr(kPcm5122Address, I2C_MASTER_WRITE) + .write_ack(reg, val) + .stop(); + // TODO: Retry once? + ESP_ERROR_CHECK(transaction.Execute()); +} + +} // namespace gay_ipod diff --git a/src/drivers/display-init.cpp b/src/drivers/display-init.cpp new file mode 100644 index 00000000..e4545339 --- /dev/null +++ b/src/drivers/display-init.cpp @@ -0,0 +1,103 @@ +#include "display-init.hpp" + +namespace gay_ipod { +namespace displays { + +/* Bit to use to signify we should delay after part of an init sequence */ +const uint8_t kDelayBit = 0x80; + +// ST7735 commands and general format from the Adafruit library for these +// displays. AFAICT it's the most complete implementation out there, and I +// really don't want to have to derive this from the datasheet myself. +// See https://github.com/adafruit/Adafruit-ST7735-Library/ + +// clang-format off +static const uint8_t kST7735RCommonHeader[]{ + 15, // 15 commands in list: + ST77XX_SWRESET, kDelayBit, // 1: Software reset, 0 args, w/delay + 150, // 150 ms delay + ST77XX_SLPOUT, kDelayBit, // 2: Out of sleep mode, 0 args, w/delay + 255, // 500 ms delay + ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + 0x01, 0x2C, 0x2D, // Dot inversion mode + 0x01, 0x2C, 0x2D, // Line inversion mode + ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: + 0x07, // No inversion + ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: + 0xA2, + 0x02, // -4.6V + 0x84, // AUTO mode + ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: + 0x0A, // Opamp current small + 0x00, // Boost frequency + ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: + 0x8A, // BCLK/2, + 0x2A, // opamp current small & medium low + ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: + 0x8A, 0xEE, + ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: + 0x0E, + ST77XX_INVOFF, 0, // 13: Don't invert display, no args + ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: + 0xC8, // row/col addr, bottom-top refresh + ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: + 0x05 +}; + +// Commands to include for the variant of the panel that has a green pull tab on +// the screen protector. +static const uint8_t kST7735RCommonGreen[]{ + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 0 + 0x00, 0x7F+0x02, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x01, // XSTART = 0 + 0x00, 0x9F+0x01}; + +// Commands to include for the variant of the panel that has a red pull tab on +// the screen protector. +static const uint8_t kST7735RCommonRed[]{ + 3, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F, + ST77XX_MADCTL, 1, + 0xC0, +}; + +static const uint8_t kST7735RCommonFooter[]{ + 4, // 4 commands in list: + ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + 0x37, 0x32, 0x29, 0x2d, // accurate colors) + 0x29, 0x25, 0x2B, 0x39, + 0x00, 0x01, 0x03, 0x10, + ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + 0x2E, 0x2E, 0x37, 0x3F, + 0x00, 0x00, 0x02, 0x10, + ST77XX_NORON, kDelayBit, // 3: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, kDelayBit, // 4: Main screen turn on, no args w/delay + 100 +}; +// clang-format on + +const InitialisationData kST7735R = { + .num_sequences = 3, + .sequences = {kST7735RCommonHeader, kST7735RCommonRed, + kST7735RCommonFooter}}; + +} // namespace displays +} // namespace gay_ipod diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp new file mode 100644 index 00000000..8708436f --- /dev/null +++ b/src/drivers/display.cpp @@ -0,0 +1,285 @@ +#include "display.hpp" +#include +#include +#include +#include +#include +#include "assert.h" +#include "display-init.hpp" +#include "driver/gpio.h" +#include "driver/spi_master.h" +#include "esp_attr.h" +#include "esp_heap_caps.h" +#include "freertos/portable.h" +#include "freertos/portmacro.h" +#include "freertos/projdefs.h" +#include "hal/gpio_types.h" +#include "hal/spi_types.h" +#include "lvgl/lvgl.h" + +static const char* kTag = "DISPLAY"; +static const gpio_num_t kCommandOrDataPin = GPIO_NUM_21; +static const gpio_num_t kLedPin = GPIO_NUM_22; + +static const uint8_t kDisplayWidth = 128; +static const uint8_t kDisplayHeight = 160; +static const uint8_t kTransactionQueueSize = 10; + +/* + * The size of each of our two display buffers. This is fundamentally a balance + * between performance and memory usage. LVGL docs recommend a buffer 1/10th the + * size of the screen is the best tradeoff. + * We use two buffers so that one can be flushed to the screen at the same time + * as the other is being drawn. + */ +static const int kDisplayBufferSize = (kDisplayWidth * kDisplayHeight) / 10; + +// Allocate both buffers in static memory to ensure that they're in DRAM, with +// minimal fragmentation. We most cases we always need these buffers anyway, so +// it's not a memory hit we can avoid anyway. +// Note: 128 * 160 / 10 * 2 bpp * 2 buffers = 8 KiB +DMA_ATTR static lv_color_t sBuffer1[kDisplayBufferSize]; +DMA_ATTR static lv_color_t sBuffer2[kDisplayBufferSize]; + +namespace gay_ipod { + +// Static functions for interrop with the LVGL display driver API, which +// requires a function pointer. +namespace callback { +static std::atomic instance = nullptr; + +static void flush_cb(lv_disp_drv_t* disp_drv, + const lv_area_t* area, + lv_color_t* color_map) { + auto instance_unwrapped = instance.load(); + if (instance_unwrapped == nullptr) { + ESP_LOGW(kTag, "uncaught flush callback"); + return; + } + // TODO: what if a transaction comes in right now? + instance_unwrapped->Flush(disp_drv, area, color_map); +} + +static void IRAM_ATTR post_cb(spi_transaction_t* transaction) { + auto instance_unwrapped = instance.load(); + if (instance_unwrapped == nullptr) { + // Can't log in ISR. + return; + } + instance_unwrapped->PostTransaction(*transaction); +} +} // namespace callback + +auto Display::create(GpioExpander* expander, + const displays::InitialisationData& init_data) + -> cpp::result, Error> { + // First, set up our GPIOs + gpio_config_t gpio_cfg = { + .pin_bit_mask = GPIO_SEL_22 | GPIO_SEL_21, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + gpio_config(&gpio_cfg); + + gpio_set_level(kLedPin, 1); + gpio_set_level(kCommandOrDataPin, 0); + + // Next, init the SPI device + spi_device_interface_config_t spi_cfg = { + .command_bits = 0, // No command phase + .address_bits = 0, // No address phase + .dummy_bits = 0, + // For ST7789, mode should be 2 + .mode = 0, + .duty_cycle_pos = 0, // Unused + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .clock_speed_hz = SPI_MASTER_FREQ_40M, + .input_delay_ns = 0, + .spics_io_num = -1, // TODO: change for R2 + .flags = 0, + .queue_size = kTransactionQueueSize, + .pre_cb = NULL, + .post_cb = &callback::post_cb, + }; + spi_device_handle_t handle; + spi_bus_add_device(VSPI_HOST, &spi_cfg, &handle); + + // TODO: ideally create this later? a bit awkward rn. + auto display = std::make_unique(expander, handle); + + // Now we reset the display into a known state, then configure it + // TODO: set rotatoin + ESP_LOGI(kTag, "Sending init sequences"); + for (int i = 0; i < init_data.num_sequences; i++) { + display->SendInitialisationSequence(init_data.sequences[i]); + } + + // The hardware is now configured correctly. Next, initialise the LVGL display + // driver. + ESP_LOGI(kTag, "Init buffers"); + lv_disp_draw_buf_init(&display->buffers_, sBuffer1, sBuffer2, + kDisplayBufferSize); + lv_disp_drv_init(&display->driver_); + display->driver_.draw_buf = &display->buffers_; + display->driver_.hor_res = kDisplayWidth; + display->driver_.ver_res = kDisplayHeight; + display->driver_.flush_cb = &callback::flush_cb; + + ESP_LOGI(kTag, "Registering driver"); + display->display_ = lv_disp_drv_register(&display->driver_); + + return std::move(display); +} + +Display::Display(GpioExpander* gpio, spi_device_handle_t handle) + : gpio_(gpio), handle_(handle) { + callback::instance = this; +} + +Display::~Display() { + callback::instance = nullptr; + // TODO. +} + +void Display::SendInitialisationSequence(const uint8_t* data) { + uint8_t command, num_args; + uint16_t sleep_duration_ms; + + // First byte of the data is the number of commands. + for (int i = *(data++); i > 0; i--) { + command = *(data++); + num_args = *(data++); + bool has_delay = (num_args & displays::kDelayBit) > 0; + num_args &= ~displays::kDelayBit; + + SendCommandWithData(command, data, num_args); + + data += num_args; + if (has_delay) { + sleep_duration_ms = *(data++); + if (sleep_duration_ms == 0xFF) { + sleep_duration_ms = 500; + } + vTaskDelay(pdMS_TO_TICKS(sleep_duration_ms)); + } + } +} + +void Display::SendCommandWithData(uint8_t command, + const uint8_t* data, + size_t length, + uintptr_t flags) { + SendCmd(&command, 1, flags); + SendData(data, length, flags); +} + +void Display::SendCmd(const uint8_t* data, size_t length, uintptr_t flags) { + SendTransaction(COMMAND, data, length, flags); +} + +void Display::SendData(const uint8_t* data, size_t length, uintptr_t flags) { + SendTransaction(DATA, data, length, flags); +} + +void Display::SendTransaction(TransactionType type, + const uint8_t* data, + size_t length, + uint32_t flags) { + if (length == 0) { + return; + } + + // TODO: Use a memory pool for these. + spi_transaction_t* transaction = (spi_transaction_t*)heap_caps_calloc( + 1, sizeof(spi_transaction_t), MALLOC_CAP_DMA); + + transaction->rx_buffer = NULL; + // Length is in bits, so multiply by 8. + transaction->length = length * 8; + transaction->rxlength = 0; // Match `length` value. + + // If the data to transmit is very short, then we can fit it directly + // inside the transaction struct. + if (length * 8 <= 32) { + transaction->flags = SPI_TRANS_USE_TXDATA; + std::memcpy(&transaction->tx_data, data, length); + } else { + // TODO: copy data to a DMA-capable transaction buffer + transaction->tx_buffer = const_cast(data); + } + + transaction->user = reinterpret_cast(flags); + + // TODO: acquire the bus first? Or in an outer scope? + // TODO: fail gracefully + // ESP_ERROR_CHECK(spi_device_queue_trans(handle_, transaction, + // portMAX_DELAY)); + // + + ServiceTransactions(); + gpio_set_level(kCommandOrDataPin, type); + + gpio_->with([&](auto& gpio_) { + gpio_.set_pin(GpioExpander::DISPLAY_CHIP_SELECT, 0); + }); + { + // auto lock = gpio_->AcquireSpiBus(GpioExpander::DISPLAY); + ESP_ERROR_CHECK(spi_device_polling_transmit(handle_, transaction)); + } + + free(transaction); +} + +void Display::Flush(lv_disp_drv_t* disp_drv, + const lv_area_t* area, + lv_color_t* color_map) { + uint16_t data[2] = {0, 0}; + + data[0] = SPI_SWAP_DATA_TX(area->x1, 16); + data[1] = SPI_SWAP_DATA_TX(area->x2, 16); + SendCommandWithData(displays::ST77XX_CASET, (uint8_t*)data, 4); + + data[0] = SPI_SWAP_DATA_TX(area->y1, 16); + data[1] = SPI_SWAP_DATA_TX(area->y2, 16); + SendCommandWithData(displays::ST77XX_RASET, (uint8_t*)data, 4); + + uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); + SendCommandWithData(displays::ST77XX_RAMWR, (uint8_t*)color_map, size * 2, + LVGL_FLUSH); + + // ESP_LOGI(kTag, "finished flush."); + // lv_disp_flush_ready(&driver_); +} + +void IRAM_ATTR Display::PostTransaction(const spi_transaction_t& transaction) { + if (reinterpret_cast(transaction.user) & LVGL_FLUSH) { + lv_disp_flush_ready(&driver_); + } +} + +void Display::ServiceTransactions() { + // todo + if (1) + return; + spi_transaction_t* transaction = nullptr; + // TODO: just wait '1' here, provide mechanism to wait for sure (poll?) + while (spi_device_get_trans_result(handle_, &transaction, pdMS_TO_TICKS(1)) != + ESP_ERR_TIMEOUT) { + ESP_LOGI(kTag, "cleaning up finished transaction"); + + // TODO: a bit dodge lmao + // TODO: also this should happen in the post callback instead i guess? + if (transaction->length > 1000) { + ESP_LOGI(kTag, "finished flush."); + lv_disp_flush_ready(&driver_); + } + + // TODO: place back into pool. + free(transaction); + } +} + +} // namespace gay_ipod diff --git a/src/drivers/gpio-expander.cpp b/src/drivers/gpio-expander.cpp new file mode 100644 index 00000000..6b472d1c --- /dev/null +++ b/src/drivers/gpio-expander.cpp @@ -0,0 +1,87 @@ +#include "gpio-expander.hpp" + +#include "i2c.hpp" + +#include + +namespace gay_ipod { + +GpioExpander::GpioExpander() { + ports_ = pack(kPortADefault, kPortBDefault); + // Read and write initial values on initialisation so that we do not have a + // strange partially-initialised state. + // TODO: log or abort if these error; it's really bad! + Write(); + Read(); +} + +GpioExpander::~GpioExpander() {} + +void GpioExpander::with(std::function f) { + f(*this); + Write(); +} + +esp_err_t GpioExpander::Write() { + i2c_cmd_handle_t handle = i2c_cmd_link_create(); + if (handle == NULL) { + return ESP_ERR_NO_MEM; + } + + std::pair ports_ab = unpack(ports()); + + I2CTransaction transaction; + transaction.start() + .write_addr(kPca8575Address, I2C_MASTER_WRITE) + .write_ack(ports_ab.first, ports_ab.second) + .stop(); + + return transaction.Execute(); +} + +esp_err_t GpioExpander::Read() { + uint8_t input_a, input_b; + + I2CTransaction transaction; + transaction.start() + .write_addr(kPca8575Address, I2C_MASTER_READ) + .read(&input_a, I2C_MASTER_ACK) + .read(&input_b, I2C_MASTER_LAST_NACK) + .stop(); + + esp_err_t ret = transaction.Execute(); + inputs_ = pack(input_a, input_b); + return ret; +} + +void GpioExpander::set_pin(ChipSelect cs, bool value) { + set_pin((Pin)cs, value); +} + +void GpioExpander::set_pin(Pin pin, bool value) { + if (value) { + ports_ |= (1 << pin); + } else { + ports_ &= ~(1 << pin); + } +} + +bool GpioExpander::get_input(Pin pin) const { + return (inputs_ & (1 << pin)) > 0; +} + +GpioExpander::SpiLock GpioExpander::AcquireSpiBus(ChipSelect cs) { + // TODO: also spi_device_acquire_bus? + return SpiLock(*this, cs); +} + +GpioExpander::SpiLock::SpiLock(GpioExpander& gpio, ChipSelect cs) + : lock_(gpio.cs_mutex_), gpio_(gpio), cs_(cs) { + gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 0); }); +} + +GpioExpander::SpiLock::~SpiLock() { + gpio_.with([&](auto& gpio) { gpio.set_pin(cs_, 1); }); +} + +} // namespace gay_ipod diff --git a/src/drivers/i2c.cpp b/src/drivers/i2c.cpp new file mode 100644 index 00000000..d3bfaa59 --- /dev/null +++ b/src/drivers/i2c.cpp @@ -0,0 +1,51 @@ +#include "i2c.hpp" +#include + +#include "assert.h" +#include "driver/i2c.h" + +namespace gay_ipod { + +static constexpr int kCmdLinkSize = I2C_LINK_RECOMMENDED_SIZE(12); + +I2CTransaction::I2CTransaction() { + // Use a fixed size buffer to avoid many many tiny allocations. + buffer_ = (uint8_t*)calloc(sizeof(uint8_t), kCmdLinkSize); + handle_ = i2c_cmd_link_create_static(buffer_, kCmdLinkSize); + assert(handle_ != NULL && "failed to create command link"); +} + +I2CTransaction::~I2CTransaction() { + free(buffer_); +} + +esp_err_t I2CTransaction::Execute() { + return i2c_master_cmd_begin(I2C_NUM_0, handle_, kI2CTimeout); +} + +I2CTransaction& I2CTransaction::start() { + ESP_ERROR_CHECK(i2c_master_start(handle_)); + return *this; +} + +I2CTransaction& I2CTransaction::stop() { + ESP_ERROR_CHECK(i2c_master_stop(handle_)); + return *this; +} + +I2CTransaction& I2CTransaction::write_addr(uint8_t addr, uint8_t op) { + write_ack(addr << 1 | op); + return *this; +} + +I2CTransaction& I2CTransaction::write_ack(uint8_t data) { + ESP_ERROR_CHECK(i2c_master_write_byte(handle_, data, true)); + return *this; +} + +I2CTransaction& I2CTransaction::read(uint8_t* dest, i2c_ack_type_t ack) { + ESP_ERROR_CHECK(i2c_master_read_byte(handle_, dest, ack)); + return *this; +} + +} // namespace gay_ipod diff --git a/src/drivers/include/battery.hpp b/src/drivers/include/battery.hpp new file mode 100644 index 00000000..399e866f --- /dev/null +++ b/src/drivers/include/battery.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "esp_err.h" + +namespace gay_ipod { + +esp_err_t init_adc(void); + +/** + * Returns the current battery level in millivolts. + */ +uint32_t read_battery_voltage(void); + +} // namespace gay_ipod diff --git a/src/drivers/include/dac.hpp b/src/drivers/include/dac.hpp new file mode 100644 index 00000000..6d025384 --- /dev/null +++ b/src/drivers/include/dac.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "gpio-expander.hpp" + +#include +#include + +#include "esp_err.h" +#include "result.hpp" + +namespace gay_ipod { + +/** + * Interface for a PCM5122PWR DAC, configured over I2C. + */ +class AudioDac { + public: + enum Error { + FAILED_TO_BOOT, + FAILED_TO_CONFIGURE, + }; + static auto create(GpioExpander* expander) + -> cpp::result, Error>; + + AudioDac(GpioExpander* gpio); + ~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 ReadPowerState(); + + // Not copyable or movable. + AudioDac(const AudioDac&) = delete; + AudioDac& operator=(const AudioDac&) = delete; + + private: + GpioExpander* gpio_; + + /* + * Pools the power state for up to 10ms, waiting for the given predicate to + * be true. + */ + bool WaitForPowerState(std::function predicate); + + enum Register { + PAGE_SELECT = 0, + DE_EMPHASIS = 7, + DIGITAL_VOLUME_L = 61, + DIGITAL_VOLUME_R = 62, + DSP_BOOT_POWER_STATE = 118, + }; + + void WriteRegister(Register reg, uint8_t val); +}; + +} // namespace gay_ipod diff --git a/src/drivers/include/display-init.hpp b/src/drivers/include/display-init.hpp new file mode 100644 index 00000000..f11e9b57 --- /dev/null +++ b/src/drivers/include/display-init.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include + +namespace gay_ipod { +namespace displays { + +extern const uint8_t kDelayBit; + +struct InitialisationData { + uint8_t num_sequences; + const uint8_t* sequences[4]; +}; + +extern const InitialisationData kST7735R; + +/* + * Valid command bytes that can be sent to ST77XX displays, as well as commands + * for more specific variants. + */ +enum StCommands { + ST77XX_NOP = 0x00, + ST77XX_SWRESET = 0x01, + ST77XX_RDDID = 0x04, + ST77XX_RDDST = 0x09, + + ST77XX_SLPIN = 0x10, + ST77XX_SLPOUT = 0x11, + ST77XX_PTLON = 0x12, + ST77XX_NORON = 0x13, + + ST77XX_INVOFF = 0x20, + ST77XX_INVON = 0x21, + ST77XX_DISPOFF = 0x28, + ST77XX_DISPON = 0x29, + ST77XX_CASET = 0x2A, + ST77XX_RASET = 0x2B, + ST77XX_RAMWR = 0x2C, + ST77XX_RAMRD = 0x2E, + + ST77XX_PTLAR = 0x30, + ST77XX_TEOFF = 0x34, + ST77XX_TEON = 0x35, + ST77XX_MADCTL = 0x36, + ST77XX_COLMOD = 0x3A, + + ST77XX_MADCTL_MY = 0x80, + ST77XX_MADCTL_MX = 0x40, + ST77XX_MADCTL_MV = 0x20, + ST77XX_MADCTL_ML = 0x10, + ST77XX_MADCTL_RGB = 0x00, + + ST77XX_RDID1 = 0xDA, + ST77XX_RDID2 = 0xDB, + ST77XX_RDID3 = 0xDC, + ST77XX_RDID4 = 0xDD, + + ST7735_MADCTL_BGR = 0x08, + ST7735_MADCTL_MH = 0x04, + + ST7735_FRMCTR1 = 0xB1, + ST7735_FRMCTR2 = 0xB2, + ST7735_FRMCTR3 = 0xB3, + ST7735_INVCTR = 0xB4, + ST7735_DISSET5 = 0xB6, + + ST7735_PWCTR1 = 0xC0, + ST7735_PWCTR2 = 0xC1, + ST7735_PWCTR3 = 0xC2, + ST7735_PWCTR4 = 0xC3, + ST7735_PWCTR5 = 0xC4, + ST7735_VMCTR1 = 0xC5, + + ST7735_PWCTR6 = 0xFC, + + ST7735_GMCTRP1 = 0xE0, + ST7735_GMCTRN1 = 0xE1, +}; + +} // namespace displays +} // namespace gay_ipod diff --git a/src/drivers/include/display.hpp b/src/drivers/include/display.hpp new file mode 100644 index 00000000..2d6e9cd6 --- /dev/null +++ b/src/drivers/include/display.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include "display-init.hpp" +#include "driver/spi_master.h" +#include "gpio-expander.hpp" +#include "lvgl/lvgl.h" +#include "result.hpp" + +namespace gay_ipod { + +/* + * Display driver for LVGL. + */ +class Display { + public: + enum Error {}; + static auto create(GpioExpander* expander, + const displays::InitialisationData& init_data) + -> cpp::result, Error>; + + Display(GpioExpander* gpio, spi_device_handle_t handle); + ~Display(); + + void WriteData(); + + void Flush(lv_disp_drv_t* disp_drv, + const lv_area_t* area, + lv_color_t* color_map); + + void IRAM_ATTR PostTransaction(const spi_transaction_t& transaction); + + void ServiceTransactions(); + + private: + GpioExpander* gpio_; + spi_device_handle_t handle_; + + lv_disp_draw_buf_t buffers_; + lv_disp_drv_t driver_; + lv_disp_t* display_ = nullptr; + + enum TransactionType { + COMMAND = 0, + DATA = 1, + }; + + enum TransactionFlags { + LVGL_FLUSH = 1, + }; + + void SendInitialisationSequence(const uint8_t* data); + + void SendCommandWithData(uint8_t command, + const uint8_t* data, + size_t length, + uintptr_t flags = 0); + + void SendCmd(const uint8_t* data, size_t length, uintptr_t flags = 0); + void SendData(const uint8_t* data, size_t length, uintptr_t flags = 0); + void SendTransaction(TransactionType type, + const uint8_t* data, + size_t length, + uintptr_t flags = 0); +}; + +} // namespace gay_ipod diff --git a/src/drivers/include/gpio-expander.hpp b/src/drivers/include/gpio-expander.hpp new file mode 100644 index 00000000..2a12fba4 --- /dev/null +++ b/src/drivers/include/gpio-expander.hpp @@ -0,0 +1,208 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "driver/i2c.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" + +namespace gay_ipod { + +/** + * Wrapper for interfacing with the PCA8575 GPIO expander. Includes basic + * low-level pin setting methods, as well as higher level convenience functions + * for reading, writing, and atomically interacting with the SPI chip select + * pins. + * + * Each method of this class can be called safely from any thread, and all + * updates are guaranteed to be atomic. Any access to chip select related pins + * should be done whilst holding `cs_lock` (preferably via the helper methods). + */ +class GpioExpander { + public: + GpioExpander(); + ~GpioExpander(); + + static const uint8_t kPca8575Address = 0x20; + static const uint8_t kPca8575Timeout = 100 / portTICK_RATE_MS; + + // Port A: + // 0 - audio power enable + // 1 - usb interface power enable + // 2 - display power enable + // 3 - sd card power enable + // 4 - charge power ok (active low) + // 5 - sd mux switch + // 6 - sd chip select + // 7 - display chip select + // All power switches low, chip selects high, active-low charge power high + static const uint8_t kPortADefault = 0b11010001; + + // Port B: + // 0 - 3.5mm jack detect (active low) + // 1 - dac soft mute switch + // 2 - GPIO + // 3 - GPIO + // 4 - GPIO + // 5 - GPIO + // 6 - GPIO + // 7 - GPIO + // DAC mute output low, everything else is active-low inputs. + static const uint8_t kPortBDefault = 0b11111111; + + /* + * Convenience mehod for packing the port a and b bytes into a single 16 bit + * value. + */ + static uint16_t pack(uint8_t a, uint8_t b) { return ((uint16_t)b) << 8 | a; } + + /* + * Convenience mehod for unpacking the result of `pack` back into two single + * byte port datas. + */ + static std::pair 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: + * + * ``` + * gpio_.with([&](auto& gpio) { + * gpio.set_pin(AUDIO_POWER_ENABLE, true); + * }); + * ``` + */ + void with(std::function f); + + /** + * Sets the ports on the GPIO expander to the values currently represented + * in `ports`. + */ + esp_err_t Write(void); + + /** + * Reads from the GPIO expander, populating `inputs` with the most recent + * values. + */ + esp_err_t Read(void); + + /* Maps each pin of the expander to its number in a `pack`ed uint16. */ + enum Pin { + // Port A + AUDIO_POWER_ENABLE = 0, + USB_INTERFACE_POWER_ENABLE = 1, + DISPLAY_POWER_ENABLE = 2, + SD_CARD_POWER_ENABLE = 3, + CHARGE_POWER_OK = 4, // Active-low input + SD_MUX_SWITCH = 5, + SD_CHIP_SELECT = 6, + DISPLAY_CHIP_SELECT = 7, + + // Port B + PHONE_DETECT = 8, // Active-high input + DAC_MUTE = 9, + GPIO_1 = 10, + GPIO_2 = 11, + GPIO_3 = 12, + GPIO_4 = 13, + GPIO_5 = 14, + GPIO_6 = 15, + }; + + /* Pins whose access should be guarded by `cs_lock`. */ + enum ChipSelect { + SD_CARD = SD_CHIP_SELECT, + DISPLAY = DISPLAY_CHIP_SELECT, + }; + + /* Nicer value names for use with the SD_MUX_SWITCH pin. */ + enum SdController { + 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. + */ + std::atomic& ports() { return ports_; } + + /* + * 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. + */ + 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. + */ + const std::atomic& 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 + * `ports()`. + */ + 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_; } + + /* + * Helper class containing an active `cs_mutex` lock. When an instance of + * 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: + SpiLock(GpioExpander& gpio, ChipSelect cs); + ~SpiLock(); + + SpiLock(const SpiLock&) = delete; + + private: + std::scoped_lock lock_; + GpioExpander& gpio_; + ChipSelect cs_; + }; + + /* + * 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 + * recommended way to safely interact with devices on the SPI bus is to have + * a self-contained block like so: + * + * ``` + * { + * auto lock = AcquireSpiBus(WHATEVER); + * // Do some cool things here. + * } + * ``` + */ + SpiLock AcquireSpiBus(ChipSelect cs); + + // Not copyable or movable. There should usually only ever be once instance + // of this class, and that instance will likely have a static lifetime. + GpioExpander(const GpioExpander&) = delete; + GpioExpander& operator=(const GpioExpander&) = delete; + + private: + std::mutex cs_mutex_; + std::atomic ports_; + std::atomic inputs_; +}; + +} // namespace gay_ipod diff --git a/src/drivers/include/i2c.hpp b/src/drivers/include/i2c.hpp new file mode 100644 index 00000000..db554f5d --- /dev/null +++ b/src/drivers/include/i2c.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include "driver/i2c.h" +#include "hal/i2c_types.h" + +namespace gay_ipod { + +/* + * Convenience wrapper for performing an I2C transaction with a reasonable + * preconfigured timeout, automatic management of a heap-based command buffer, + * and a terser API for enqueuing bytes. + * + * Any error codes from the underlying ESP IDF are treated as fatal, since they + * typically represent invalid arguments or OOMs. + */ +class I2CTransaction { + public: + static const uint8_t kI2CTimeout = 100 / portTICK_RATE_MS; + + 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 doesn’t 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(); + + /* + * Enqueues a start condition. May also be used for repeated start + * conditions. + */ + I2CTransaction& start(); + /* Enqueues a stop condition. */ + I2CTransaction& stop(); + + /* + * Enqueues writing the given 7 bit address, followed by one bit indicating + * whether this is a read or write request. + * + * This command will expect an ACK before continuing. + */ + I2CTransaction& write_addr(uint8_t addr, uint8_t op); + + /* + * Enqueues one or more bytes to be written. The transaction will wait for + * an ACK to be returned before writing the next byte. + */ + I2CTransaction& write_ack(uint8_t data); + template + I2CTransaction& write_ack(uint8_t data, More... more) { + write_ack(data); + write_ack(more...); + return *this; + } + + /* + * Enqueues a read of one byte into the given uint8. Responds with the given + * ACK/NACK type. + */ + I2CTransaction& read(uint8_t* dest, i2c_ack_type_t ack); + + /* Returns the underlying command buffer. */ + i2c_cmd_handle_t handle() { return handle_; } + + // Cannot be moved or copied, since doing so is probably an error. Pass a + // reference instead. + I2CTransaction(const I2CTransaction&) = delete; + I2CTransaction& operator=(const I2CTransaction&) = delete; + + private: + i2c_cmd_handle_t handle_; + uint8_t* buffer_; +}; + +} // namespace gay_ipod diff --git a/src/drivers/include/playback.hpp b/src/drivers/include/playback.hpp new file mode 100644 index 00000000..493dd311 --- /dev/null +++ b/src/drivers/include/playback.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "dac.hpp" +#include "storage.hpp" + +#include +#include +#include + +#include "audio_common.h" +#include "audio_element.h" +#include "audio_event_iface.h" +#include "audio_pipeline.h" +#include "esp_err.h" +#include "fatfs_stream.h" +#include "i2s_stream.h" +#include "mp3_decoder.h" +#include "result.hpp" + +namespace gay_ipod { + +class DacAudioPlayback { + public: + enum Error { PIPELINE_INIT }; + static auto create(AudioDac* dac) + -> cpp::result, 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(); + + void ProcessEvents(); + + /* for gapless */ + void set_next_file(const std::string& filename); + + void set_volume(uint8_t volume); + auto volume() -> uint8_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 diff --git a/src/drivers/include/storage.hpp b/src/drivers/include/storage.hpp new file mode 100644 index 00000000..cee49cc5 --- /dev/null +++ b/src/drivers/include/storage.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "gpio-expander.hpp" + +#include + +#include "driver/sdmmc_types.h" +#include "driver/sdspi_host.h" +#include "esp_err.h" +#include "esp_vfs_fat.h" +#include "result.hpp" + +namespace gay_ipod { + +extern const char* kStoragePath; + +class SdStorage { + public: + enum Error { + FAILED_TO_INIT, + /** We couldn't interact with the SD card at all. Is it missing? */ + FAILED_TO_READ, + /** We couldn't mount the SD card. Is it formatted? */ + FAILED_TO_MOUNT, + }; + + static auto create(GpioExpander* gpio) + -> cpp::result, Error>; + + SdStorage(GpioExpander* gpio, + esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*), + sdspi_dev_handle_t handle_, + std::unique_ptr& host_, + std::unique_ptr& card_, + FATFS* fs_); + ~SdStorage(); + + auto HandleTransaction(sdspi_dev_handle_t handle, sdmmc_command_t* cmdinfo) + -> esp_err_t; + + // Not copyable or movable. + // TODO: maybe this could be movable? + SdStorage(const SdStorage&) = delete; + SdStorage& operator=(const SdStorage&) = delete; + + private: + GpioExpander* gpio_; + + esp_err_t (*do_transaction_)(sdspi_dev_handle_t, sdmmc_command_t*) = nullptr; + + // SPI and SD driver info + sdspi_dev_handle_t handle_; + std::unique_ptr host_; + std::unique_ptr card_; + + // Filesystem info + FATFS* fs_ = nullptr; +}; + +} // namespace gay_ipod diff --git a/src/drivers/playback.cpp b/src/drivers/playback.cpp new file mode 100644 index 00000000..46dec680 --- /dev/null +++ b/src/drivers/playback.cpp @@ -0,0 +1,246 @@ +#include "playback.hpp" + +#include "dac.hpp" + +#include + +#include "audio_element.h" +#include "audio_event_iface.h" +#include "audio_pipeline.h" +#include "driver/i2s.h" +#include "esp_err.h" +#include "freertos/portmacro.h" +#include "mp3_decoder.h" + +static const char* kTag = "PLAYBACK"; +static const i2s_port_t kI2SPort = I2S_NUM_0; + +namespace gay_ipod { + +static audio_element_status_t status_from_the_void(void* status) { + uintptr_t as_pointer_int = reinterpret_cast(status); + return static_cast(as_pointer_int); +} + +auto DacAudioPlayback::create(AudioDac* dac) + -> cpp::result, Error> { + // 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_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_interface = audio_event_iface_init(&event_config); + + audio_pipeline_set_listener(pipeline, event_interface); + audio_element_msg_set_listener(fatfs_stream_reader, event_interface); + audio_element_msg_set_listener(mp3_decoder, event_interface); + audio_element_msg_set_listener(i2s_stream_writer, 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(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) {} + +DacAudioPlayback::~DacAudioPlayback() { + dac_->WriteVolume(255); + + audio_pipeline_remove_listener(pipeline_); + audio_element_msg_remove_listener(fatfs_stream_reader_, event_interface_); + audio_element_msg_remove_listener(mp3_decoder_, event_interface_); + audio_element_msg_remove_listener(i2s_stream_writer_, event_interface_); + + 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::ProcessEvents() { + while (1) { + audio_event_iface_msg_t event; + esp_err_t err = + audio_event_iface_listen(event_interface_, &event, portMAX_DELAY); + if (err != ESP_OK) { + ESP_LOGI(kTag, "error listening for event:%x", err); + continue; + } + ESP_LOGI(kTag, "received event, cmd %i", event.cmd); + + if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && + event.source == (void*)mp3_decoder_ && + event.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { + audio_element_info_t music_info = {0}; + audio_element_getinfo(mp3_decoder_, &music_info); + ESP_LOGI(kTag, "sample_rate=%d, bits=%d, ch=%d", music_info.sample_rates, + music_info.bits, music_info.channels); + audio_element_setinfo(i2s_stream_writer_, &music_info); + i2s_stream_set_clk(i2s_stream_writer_, music_info.sample_rates, + music_info.bits, music_info.channels); + } + + if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && + event.source == (void*)fatfs_stream_reader_ && + event.cmd == AEL_MSG_CMD_REPORT_STATUS) { + audio_element_status_t status = status_from_the_void(event.data); + if (status == AEL_STATUS_STATE_FINISHED) { + // TODO: enqueue next track? + } + } + + if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && + event.source == (void*)i2s_stream_writer_ && + event.cmd == AEL_MSG_CMD_REPORT_STATUS) { + audio_element_status_t status = status_from_the_void(event.data); + if (status == AEL_STATUS_STATE_FINISHED) { + // TODO. + return; + } + } + + if (event.need_free_data) { + ESP_LOGI(kTag, "freeing event data"); + free(event.data); + } + } +} + +/* 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_; +} + +} // namespace gay_ipod diff --git a/src/drivers/storage.cpp b/src/drivers/storage.cpp new file mode 100644 index 00000000..7fed6ed0 --- /dev/null +++ b/src/drivers/storage.cpp @@ -0,0 +1,149 @@ +#include "storage.hpp" + +#include "gpio-expander.hpp" + +#include +#include +#include + +#include "diskio_impl.h" +#include "diskio_sdmmc.h" +#include "driver/gpio.h" +#include "driver/sdmmc_types.h" +#include "driver/sdspi_host.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_vfs_fat.h" +#include "ff.h" +#include "hal/gpio_types.h" +#include "hal/spi_types.h" +#include "sdmmc_cmd.h" + +static const char* kTag = "SDSTORAGE"; +static const uint8_t kMaxOpenFiles = 8; + +namespace gay_ipod { + +const char* kStoragePath = "/sdcard"; + +// Static functions for interrop with the ESP IDF API, which requires a +// function pointer. +namespace callback { +static std::atomic instance = nullptr; +static std::atomic + bootstrap = nullptr; + +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 + +auto SdStorage::create(GpioExpander* gpio) + -> cpp::result, Error> { + // Acquiring the bus will also flush the mux switch change. + gpio->set_pin(GpioExpander::SD_MUX_SWITCH, GpioExpander::SD_MUX_ESP); + + sdspi_dev_handle_t handle; + std::unique_ptr host; + std::unique_ptr card; + FATFS* fs = nullptr; + + // Now we can init the driver and set up the SD card into SPI mode. + sdspi_host_init(); + + sdspi_device_config_t config = { + .host_id = VSPI_HOST, + // CS handled manually bc it's on the GPIO expander + .gpio_cs = GPIO_NUM_2, + .gpio_cd = SDSPI_SLOT_NO_CD, + .gpio_wp = SDSPI_SLOT_NO_WP, + .gpio_int = GPIO_NUM_NC, + }; + 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 = std::make_unique(sdmmc_host_t SDSPI_HOST_DEFAULT()); + card = std::make_unique(); + + // 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 + // with our own that acquires the CS mutex for the duration of the SPI + // transaction. + auto do_transaction = host->do_transaction; + host->do_transaction = &callback::do_transaction; + host->slot = handle; + callback::bootstrap = do_transaction; + + auto lock = gpio->AcquireSpiBus(GpioExpander::SD_CARD); + // Will return ESP_ERR_INVALID_RESPONSE if there is no card + esp_err_t err = sdmmc_card_init(host.get(), card.get()); + if (err != ESP_OK) { + ESP_LOGW(kTag, "Failed to read, err: %d", err); + return cpp::fail(Error::FAILED_TO_READ); + } + + ESP_ERROR_CHECK(esp_vfs_fat_register(kStoragePath, "", kMaxOpenFiles, &fs)); + ff_diskio_register_sdmmc(fs->pdrv, card.get()); + + // Mount right now, not on first operation. + FRESULT ferr = f_mount(fs, "", 1); + if (ferr != FR_OK) { + ESP_LOGW(kTag, "Failed to mount, err: %d", ferr); + return cpp::fail(Error::FAILED_TO_MOUNT); + } + + return std::make_unique(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, + std::unique_ptr& host, + std::unique_ptr& card, + FATFS* fs) + : gpio_(gpio), + do_transaction_(do_transaction), + handle_(handle), + host_(std::move(host)), + card_(std::move(card)), + fs_(fs) { + callback::instance = this; + callback::bootstrap = nullptr; +} + +SdStorage::~SdStorage() { + // Unmount and unregister the filesystem + f_unmount(""); + ff_diskio_register(fs_->pdrv, NULL); + esp_vfs_fat_unregister_path(kStoragePath); + fs_ = nullptr; + + callback::instance = nullptr; + + // Uninstall the SPI driver + sdspi_host_remove_device(this->handle_); + sdspi_host_deinit(); +} + +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 diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt new file mode 100644 index 00000000..b744a447 --- /dev/null +++ b/src/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register( + SRCS "main.cpp" + REQUIRES "drivers") diff --git a/src/main/main.cpp b/src/main/main.cpp new file mode 100644 index 00000000..8d989146 --- /dev/null +++ b/src/main/main.cpp @@ -0,0 +1,201 @@ +#include "battery.hpp" +#include "core/lv_disp.h" +#include "core/lv_obj_pos.h" +#include "dac.hpp" +#include "display-init.hpp" +#include "display.hpp" +#include "esp_freertos_hooks.h" +#include "freertos/portmacro.h" +#include "gpio-expander.hpp" +#include "misc/lv_color.h" +#include "misc/lv_timer.h" +#include "playback.hpp" +#include "storage.hpp" + +#include +#include +#include +#include +#include + +#include "audio_common.h" +#include "audio_element.h" +#include "audio_pipeline.h" +#include "driver/gpio.h" +#include "driver/i2c.h" +#include "driver/sdspi_host.h" +#include "driver/spi_common.h" +#include "driver/spi_master.h" +#include "esp_intr_alloc.h" +#include "esp_log.h" +#include "hal/gpio_types.h" +#include "hal/spi_types.h" +#include "lvgl/lvgl.h" +#include "widgets/lv_label.h" + +#define I2C_SDA_IO (GPIO_NUM_2) +#define I2C_SCL_IO (GPIO_NUM_4) +#define I2C_CLOCK_HZ (400000) + +#define SPI_SDI_IO (GPIO_NUM_19) +#define SPI_SDO_IO (GPIO_NUM_23) +#define SPI_SCLK_IO (GPIO_NUM_18) +#define SPI_QUADWP_IO (GPIO_NUM_22) +#define SPI_QUADHD_IO (GPIO_NUM_21) + +static const char* TAG = "MAIN"; + +esp_err_t init_i2c(void) { + i2c_port_t port = I2C_NUM_0; + i2c_config_t config = { + .mode = I2C_MODE_MASTER, + .sda_io_num = I2C_SDA_IO, + .scl_io_num = I2C_SCL_IO, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master = + { + .clk_speed = I2C_CLOCK_HZ, + }, + // No requirements for the clock. + .clk_flags = 0, + }; + + ESP_ERROR_CHECK(i2c_param_config(port, &config)); + ESP_ERROR_CHECK(i2c_driver_install(port, config.mode, 0, 0, 0)); + + // TODO: INT line + + return ESP_OK; +} + +esp_err_t init_spi(void) { + spi_bus_config_t config = { + .mosi_io_num = SPI_SDO_IO, + .miso_io_num = SPI_SDI_IO, + .sclk_io_num = SPI_SCLK_IO, + .quadwp_io_num = -1, // SPI_QUADWP_IO, + .quadhd_io_num = -1, // SPI_QUADHD_IO, + + // Unused + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + + // Use the DMA default size. The display requires larger buffers, but it + // manages its down use of DMA-capable memory. + .max_transfer_sz = 128 * 16 * 2, // TODO: hmm + .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, + .intr_flags = 0, + }; + + ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &config, SPI_DMA_CH_AUTO)); + + return ESP_OK; +} + +void IRAM_ATTR tick_hook(void) { + lv_tick_inc(1); +} + +static const size_t kLvglStackSize = 8 * 1024; +static StaticTask_t sLvglTaskBuffer = {}; +static StackType_t sLvglStack[kLvglStackSize] = {0}; + +struct LvglArgs { + gay_ipod::GpioExpander* gpio_expander; +}; + +void lvgl_main(void* voidArgs) { + ESP_LOGI(TAG, "starting LVGL task"); + LvglArgs* args = (LvglArgs*)voidArgs; + gay_ipod::GpioExpander* gpio_expander = args->gpio_expander; + + // Dispose of the args now that we've gotten everything out of them. + delete args; + + ESP_LOGI(TAG, "init lvgl"); + lv_init(); + + // LVGL has been initialised, so we can now start reporting ticks to it. + esp_register_freertos_tick_hook(&tick_hook); + + ESP_LOGI(TAG, "init display"); + auto display_res = + gay_ipod::Display::create(gpio_expander, gay_ipod::displays::kST7735R); + if (display_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", display_res.error()); + return; + } + std::unique_ptr display = std::move(display_res.value()); + + auto label = lv_label_create(NULL); + lv_label_set_text(label, "g'day, cunts!"); + lv_obj_center(label); + lv_scr_load(label); + + while (1) { + lv_timer_handler(); + // display->ServiceTransactions(); + vTaskDelay(pdMS_TO_TICKS(10)); + } + + // TODO: break from the loop to kill this task, so that we can do our RAII + // cleanup, unregister our tick callback and so on. +} + +extern "C" void app_main(void) { + ESP_LOGI(TAG, "Initialising peripherals"); + + ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED)); + init_i2c(); + init_spi(); + ESP_ERROR_CHECK(gay_ipod::init_adc()); + + ESP_LOGI(TAG, "Init GPIOs"); + gay_ipod::GpioExpander* expander = new gay_ipod::GpioExpander(); + + // for debugging usb ic + // expander.set_sd_mux(gay_ipod::GpioExpander::USB); + + /* + ESP_LOGI(TAG, "Init SD card"); + auto storage_res = gay_ipod::SdStorage::create(expander); + if (storage_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", storage_res.error()); + return; + } + std::unique_ptr storage = std::move(storage_res.value()); + + ESP_LOGI(TAG, "Init DAC"); + auto dac_res = gay_ipod::AudioDac::create(expander); + if (storage_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", dac_res.error()); + return; + } + std::unique_ptr dac = std::move(dac_res.value()); + + ESP_LOGI(TAG, "Init Audio Pipeline"); + auto playback_res = gay_ipod::DacAudioPlayback::create(dac.get()); + if (playback_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", playback_res.error()); + return; + } + std::unique_ptr playback = + std::move(playback_res.value()); + */ + + ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); + vTaskDelay(pdMS_TO_TICKS(1500)); + + LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs)); + lvglArgs->gpio_expander = expander; + xTaskCreateStaticPinnedToCore(&lvgl_main, "LVGL", kLvglStackSize, (void*)lvglArgs, + 1, sLvglStack, &sLvglTaskBuffer, 1); + + while (1) { + // TODO: Find owners for everything so we can quit this task safely. + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} -- cgit v1.2.3