diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 36133626..805dffc4 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -11,11 +11,13 @@ #include "audio_decoder.hpp" #include "audio_events.hpp" #include "audio_task.hpp" +#include "esp_log.h" #include "event_queue.hpp" #include "fatfs_audio_input.hpp" #include "i2s_audio_output.hpp" #include "i2s_dac.hpp" #include "pipeline.hpp" +#include "system_events.hpp" #include "track.hpp" namespace audio { @@ -66,16 +68,26 @@ void AudioState::react(const system_fsm::StorageMounted& ev) { void AudioState::react(const system_fsm::KeyUpChanged& ev) { if (ev.falling && sI2SOutput->AdjustVolumeUp()) { + ESP_LOGI(kTag, "volume up!"); events::Dispatch({}); } } void AudioState::react(const system_fsm::KeyDownChanged& ev) { if (ev.falling && sI2SOutput->AdjustVolumeDown()) { + ESP_LOGI(kTag, "volume down!"); events::Dispatch({}); } } +void AudioState::react(const system_fsm::HasPhonesChanged& ev) { + if (ev.falling) { + ESP_LOGI(kTag, "headphones in!"); + } else { + ESP_LOGI(kTag, "headphones out!"); + } +} + namespace states { void Uninitialised::react(const system_fsm::BootComplete&) { diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 1a52375b..dadbd072 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -42,6 +42,7 @@ class AudioState : public tinyfsm::Fsm { void react(const system_fsm::KeyUpChanged&); void react(const system_fsm::KeyDownChanged&); + void react(const system_fsm::HasPhonesChanged&); virtual void react(const system_fsm::BootComplete&) {} virtual void react(const PlayTrack&) {} diff --git a/src/drivers/gpios.cpp b/src/drivers/gpios.cpp new file mode 100644 index 00000000..6689e2ea --- /dev/null +++ b/src/drivers/gpios.cpp @@ -0,0 +1,148 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "gpios.hpp" + +#include + +#include + +#include "driver/gpio.h" +#include "hal/gpio_types.h" +#include "i2c.hpp" + +namespace drivers { + +static const uint8_t kPca8575Address = 0x20; + +// Port A: +// 0 - sd card mux switch +// 1 - sd card mux enable (active low) +// 2 - key up +// 3 - key down +// 4 - key lock +// 5 - display reset (active low) +// 6 - NC +// 7 - sd card power (active low) +// Default to SD card off, inputs high. +static const uint8_t kPortADefault = 0b10111110; + +// Port B: +// 0 - 3.5mm jack detect (active low) +// 1 - headphone amp power enable +// 2 - volume zero-cross detection +// 3 - volume direction +// 4 - volume left channel +// 5 - volume right channel +// 6 - NC +// 7 - NC +// Default input high, trs output low +static const uint8_t kPortBDefault = 0b00000011; + +/* + * Convenience mehod for packing the port a and b bytes into a single 16 bit + * value. + */ +constexpr 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. + */ +constexpr std::pair unpack(uint16_t ba) { + return std::pair((uint8_t)ba, (uint8_t)(ba >> 8)); +} + +void interrupt_isr(void* arg) { + Gpios* instance = reinterpret_cast(arg); + auto listener = instance->listener(); + if (listener != nullptr) { + std::invoke(*listener); + } +} + +auto Gpios::Create() -> Gpios* { + Gpios* instance = new Gpios(); + // Read and write initial values on initialisation so that we do not have a + // strange partially-initialised state. + if (!instance->Flush() || !instance->Read()) { + return nullptr; + } + return instance; +} + +Gpios::Gpios() + : ports_(pack(kPortADefault, kPortBDefault)), + inputs_(0), + listener_(nullptr) { + gpio_config_t config{ + .pin_bit_mask = static_cast(1) << GPIO_NUM_34, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_NEGEDGE, + }; + gpio_config(&config); + gpio_install_isr_service(ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_SHARED | + ESP_INTR_FLAG_IRAM); + gpio_isr_handler_add(GPIO_NUM_34, &interrupt_isr, this); +} + +Gpios::~Gpios() { + gpio_isr_handler_remove(GPIO_NUM_34); + gpio_uninstall_isr_service(); +} + +auto Gpios::WriteBuffered(Pin pin, bool value) -> void { + if (value) { + ports_ |= (1 << static_cast(pin)); + } else { + ports_ &= ~(1 << static_cast(pin)); + } +} + +auto Gpios::WriteSync(Pin pin, bool value) -> bool { + WriteBuffered(pin, value); + return Flush(); +} + +auto Gpios::Flush() -> bool { + 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_OK; +} + +auto Gpios::Get(Pin pin) const -> bool { + return (inputs_ & (1 << static_cast(pin))) > 0; +} + +auto Gpios::Read() -> bool { + 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(); + if (ret != ESP_OK) { + return false; + } + inputs_ = pack(input_a, input_b); + return true; +} + +} // namespace drivers diff --git a/src/drivers/include/gpios.hpp b/src/drivers/include/gpios.hpp new file mode 100644 index 00000000..32bbacf4 --- /dev/null +++ b/src/drivers/include/gpios.hpp @@ -0,0 +1,128 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include + +#include +#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 drivers { + +/** + * 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 IGpios { + public: + virtual ~IGpios() {} + + /* Maps each pin of the expander to its number in a `pack`ed uint16. */ + enum class Pin { + // Port A + kSdMuxSwitch = 0, + kSdMuxDisable = 1, + kKeyUp = 2, + kKeyDown = 3, + kKeyLock = 4, + kDisplayEnable = 5, + // 6 is unused + kSdPowerDisable = 7, + + // Port B + kPhoneDetect = 8, + kAmplifierEnable = 9, + kSdCardDetect = 10, + // 11 through 15 are unused + }; + + /* Nicer value names for use with kSdMuxSwitch. */ + enum SdController { + SD_MUX_ESP = 0, + SD_MUX_SAMD = 1, + }; + + /* + * Sets a single specific pin to the given value. `true` corresponds to + * HIGH, and `false` corresponds to LOW. + * + * This function will block until the pin has changed level. + */ + virtual auto WriteSync(Pin, bool) -> bool = 0; + + /* + * Returns the most recently cached value of the given pin. + */ + virtual auto Get(Pin) const -> bool = 0; +}; + +class Gpios : public IGpios { + public: + static auto Create() -> Gpios*; + ~Gpios(); + + /* + * 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. + */ + auto WriteSync(Pin, bool) -> bool override; + + virtual auto WriteBuffered(Pin, bool) -> void; + + /** + * Sets the ports on the GPIO expander to the values currently represented + * in `ports`. + */ + auto Flush(void) -> bool; + + auto Get(Pin) const -> bool override; + + /** + * Reads from the GPIO expander, populating `inputs` with the most recent + * values. + */ + auto Read(void) -> bool; + + auto listener() -> std::function* { return listener_; } + + auto set_listener(std::function* l) -> void { listener_ = l; } + + // Not copyable or movable. There should usually only ever be once instance + // of this class, and that instance will likely have a static lifetime. + Gpios(const Gpios&) = delete; + Gpios& operator=(const Gpios&) = delete; + + private: + Gpios(); + + std::atomic ports_; + std::atomic inputs_; + + std::function* listener_; +}; + +} // namespace drivers diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 41adb906..dcfefbab 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -45,6 +45,8 @@ auto Booting::entry() -> void { sGpios.reset(drivers::Gpios::Create()); assert(sGpios != nullptr); + sGpios->set_listener(&sGpiosCallback); + // Start bringing up LVGL now, since we have all of its prerequisites. ESP_LOGI(kTag, "starting ui"); if (!ui::UiState::Init(sGpios.get())) { diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 9483088e..1b3aab51 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -31,7 +31,6 @@ void SystemState::react(const FatalError& err) { } void SystemState::react(const internal::GpioInterrupt& ev) { - ESP_LOGI("sys", "gpios changed"); bool prev_key_up = sGpios->Get(drivers::Gpios::Pin::kKeyUp); bool prev_key_down = sGpios->Get(drivers::Gpios::Pin::kKeyDown); bool prev_key_lock = sGpios->Get(drivers::Gpios::Pin::kKeyLock);