Split driver-y things into a separate component
This commit is contained in:
@@ -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")
|
||||
@@ -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
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "dac.hpp"
|
||||
|
||||
#include "gpio-expander.hpp"
|
||||
#include "i2c.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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<std::unique_ptr<AudioDac>, Error> {
|
||||
std::unique_ptr<AudioDac> dac = std::make_unique<AudioDac>(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<bool, AudioDac::PowerState> 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<bool(bool, AudioDac::PowerState)> predicate) {
|
||||
bool has_matched = false;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::pair<bool, PowerState> result = ReadPowerState();
|
||||
has_matched = predicate(result.first, result.second);
|
||||
if (has_matched) {
|
||||
break;
|
||||
} else {
|
||||
ESP_LOGI(kTag, "Waiting for power state (was %d %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
|
||||
@@ -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
|
||||
@@ -0,0 +1,285 @@
|
||||
#include "display.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#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<Display*> 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<std::unique_ptr<Display>, 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<Display>(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<uint8_t*>(data);
|
||||
}
|
||||
|
||||
transaction->user = reinterpret_cast<void*>(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<uintptr_t>(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
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "gpio-expander.hpp"
|
||||
|
||||
#include "i2c.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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<void(GpioExpander&)> 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<uint8_t, uint8_t> 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
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "i2c.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "gpio-expander.hpp"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <functional>
|
||||
|
||||
#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<std::unique_ptr<AudioDac>, 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<bool, PowerState> 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<bool(bool, PowerState)> 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
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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<std::unique_ptr<Display>, 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
|
||||
@@ -0,0 +1,208 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#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<uint8_t, uint8_t> unpack(uint16_t ba) {
|
||||
return std::pair((uint8_t)ba, (uint8_t)(ba >> 8));
|
||||
}
|
||||
|
||||
/*
|
||||
* Convenience function for running some arbitrary pin writing code, then
|
||||
* flushing a `Write()` to the expander. Example usage:
|
||||
*
|
||||
* ```
|
||||
* gpio_.with([&](auto& gpio) {
|
||||
* gpio.set_pin(AUDIO_POWER_ENABLE, true);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
void with(std::function<void(GpioExpander&)> 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<uint16_t>& 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<uint16_t>& inputs() const { return inputs_; }
|
||||
|
||||
/* Returns the most recently cached value of the given pin. Only valid for
|
||||
* pins used as inputs; to check what value we're driving a pin, use
|
||||
* `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<std::mutex> 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<uint16_t> ports_;
|
||||
std::atomic<uint16_t> inputs_;
|
||||
};
|
||||
|
||||
} // namespace gay_ipod
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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 <typename... More>
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "dac.hpp"
|
||||
#include "storage.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#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<std::unique_ptr<DacAudioPlayback>, Error>;
|
||||
|
||||
DacAudioPlayback(AudioDac* dac,
|
||||
audio_pipeline_handle_t pipeline,
|
||||
audio_element_handle_t fatfs_stream_reader,
|
||||
audio_element_handle_t i2s_stream_writer,
|
||||
audio_event_iface_handle_t event_interface,
|
||||
audio_element_handle_t mp3_decoder);
|
||||
~DacAudioPlayback();
|
||||
|
||||
void Play(const std::string& filename);
|
||||
void Resume();
|
||||
void Pause();
|
||||
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "gpio-expander.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#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<std::unique_ptr<SdStorage>, Error>;
|
||||
|
||||
SdStorage(GpioExpander* gpio,
|
||||
esp_err_t (*do_transaction)(sdspi_dev_handle_t, sdmmc_command_t*),
|
||||
sdspi_dev_handle_t handle_,
|
||||
std::unique_ptr<sdmmc_host_t>& host_,
|
||||
std::unique_ptr<sdmmc_card_t>& 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<sdmmc_host_t> host_;
|
||||
std::unique_ptr<sdmmc_card_t> card_;
|
||||
|
||||
// Filesystem info
|
||||
FATFS* fs_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace gay_ipod
|
||||
@@ -0,0 +1,246 @@
|
||||
#include "playback.hpp"
|
||||
|
||||
#include "dac.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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<uintptr_t>(status);
|
||||
return static_cast<audio_element_status_t>(as_pointer_int);
|
||||
}
|
||||
|
||||
auto DacAudioPlayback::create(AudioDac* dac)
|
||||
-> cpp::result<std::unique_ptr<DacAudioPlayback>, 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_t>(I2S_MODE_MASTER | I2S_MODE_TX),
|
||||
.sample_rate = 44100,
|
||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
|
||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||
.intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
|
||||
.dma_buf_count = 8,
|
||||
.dma_buf_len = 64,
|
||||
.use_apll = false,
|
||||
.tx_desc_auto_clear = false,
|
||||
.fixed_mclk = 0,
|
||||
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
|
||||
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
|
||||
},
|
||||
.i2s_port = kI2SPort,
|
||||
.use_alc = false,
|
||||
.volume = 0, // Does nothing; use AudioDac to change this.
|
||||
.out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
|
||||
.task_stack = I2S_STREAM_TASK_STACK,
|
||||
.task_core = I2S_STREAM_TASK_CORE,
|
||||
.task_prio = I2S_STREAM_TASK_PRIO,
|
||||
.stack_in_ext = false,
|
||||
.multi_out_num = 0,
|
||||
.uninstall_drv = true,
|
||||
.need_expand = false,
|
||||
.expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
|
||||
};
|
||||
i2s_stream_writer = i2s_stream_init(&i2s_stream_config);
|
||||
if (i2s_stream_writer == NULL) {
|
||||
return cpp::fail(Error::PIPELINE_INIT);
|
||||
}
|
||||
|
||||
// NOTE: i2s_stream_init does some additional setup that hardcodes MCK as
|
||||
// GPIO0. This happens to work fine for us, but be careful if changing.
|
||||
i2s_pin_config_t pin_config = {.mck_io_num = GPIO_NUM_0,
|
||||
.bck_io_num = GPIO_NUM_26,
|
||||
.ws_io_num = GPIO_NUM_27,
|
||||
.data_out_num = GPIO_NUM_5,
|
||||
.data_in_num = I2S_PIN_NO_CHANGE};
|
||||
if (esp_err_t err = i2s_set_pin(kI2SPort, &pin_config) != ESP_OK) {
|
||||
ESP_LOGE(kTag, "failed to configure i2s pins %x", err);
|
||||
return cpp::fail(Error::PIPELINE_INIT);
|
||||
}
|
||||
|
||||
// TODO: Create encoders dynamically when we need them.
|
||||
audio_element_handle_t mp3_decoder;
|
||||
mp3_decoder_cfg_t mp3_config =
|
||||
mp3_decoder_cfg_t(DEFAULT_MP3_DECODER_CONFIG());
|
||||
mp3_decoder = mp3_decoder_init(&mp3_config);
|
||||
assert(mp3_decoder != NULL);
|
||||
|
||||
audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG();
|
||||
event_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<DacAudioPlayback>(dac, pipeline, fatfs_stream_reader,
|
||||
i2s_stream_writer, event_interface,
|
||||
mp3_decoder);
|
||||
}
|
||||
|
||||
DacAudioPlayback::DacAudioPlayback(AudioDac* dac,
|
||||
audio_pipeline_handle_t pipeline,
|
||||
audio_element_handle_t fatfs_stream_reader,
|
||||
audio_element_handle_t i2s_stream_writer,
|
||||
audio_event_iface_handle_t event_interface,
|
||||
audio_element_handle_t mp3_decoder)
|
||||
: dac_(dac),
|
||||
pipeline_(pipeline),
|
||||
fatfs_stream_reader_(fatfs_stream_reader),
|
||||
i2s_stream_writer_(i2s_stream_writer),
|
||||
event_interface_(event_interface),
|
||||
mp3_decoder_(mp3_decoder) {}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "storage.hpp"
|
||||
|
||||
#include "gpio-expander.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#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<SdStorage*> instance = nullptr;
|
||||
static std::atomic<esp_err_t (*)(sdspi_dev_handle_t, sdmmc_command_t*)>
|
||||
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<std::unique_ptr<SdStorage>, 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<sdmmc_host_t> host;
|
||||
std::unique_ptr<sdmmc_card_t> 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>(sdmmc_host_t SDSPI_HOST_DEFAULT());
|
||||
card = std::make_unique<sdmmc_card_t>();
|
||||
|
||||
// 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<SdStorage>(gpio, do_transaction, handle, host, card,
|
||||
fs);
|
||||
}
|
||||
|
||||
SdStorage::SdStorage(GpioExpander* gpio,
|
||||
esp_err_t (*do_transaction)(sdspi_dev_handle_t,
|
||||
sdmmc_command_t*),
|
||||
sdspi_dev_handle_t handle,
|
||||
std::unique_ptr<sdmmc_host_t>& host,
|
||||
std::unique_ptr<sdmmc_card_t>& 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
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(
|
||||
SRCS "main.cpp"
|
||||
REQUIRES "drivers")
|
||||
@@ -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 <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#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<gay_ipod::Display> 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<gay_ipod::SdStorage> 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<gay_ipod::AudioDac> 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<gay_ipod::DacAudioPlayback> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user