mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
some refactoring
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
idf_component_register(SRCS
|
||||
src/app_main.cpp
|
||||
src/display.cpp
|
||||
src/bat_mon.cpp
|
||||
src/spi_global.cpp
|
||||
src/i2c_global.cpp
|
||||
src/shutdowner.cpp
|
||||
src/buttons.cpp
|
||||
src/power_helper.cpp
|
||||
src/buzzer.cpp
|
||||
src/fs_helper.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
|
||||
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb"
|
||||
INCLUDE_DIRS "include" "../sdk/include")
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/app_main.cpp"
|
||||
PRIV_REQUIRES
|
||||
sdk-esp
|
||||
littlefs
|
||||
REQUIRES
|
||||
backend-esp
|
||||
EMBED_FILES
|
||||
"roms/builtin_demo1.gb"
|
||||
"roms/builtin_demo2.gb"
|
||||
INCLUDE_DIRS
|
||||
""
|
||||
)
|
||||
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
|
||||
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
using AppEventType = cardboy::sdk::AppEventType;
|
||||
using AppButtonEvent = cardboy::sdk::AppButtonEvent;
|
||||
using AppTimerEvent = cardboy::sdk::AppTimerEvent;
|
||||
using AppEvent = cardboy::sdk::AppEvent;
|
||||
|
||||
using AppContext = cardboy::sdk::AppContext;
|
||||
|
||||
using IApp = cardboy::sdk::IApp;
|
||||
using IAppFactory = cardboy::sdk::IAppFactory;
|
||||
using Services = cardboy::sdk::Services;
|
||||
using IBuzzer = cardboy::sdk::IBuzzer;
|
||||
using IBatteryMonitor = cardboy::sdk::IBatteryMonitor;
|
||||
using IStorage = cardboy::sdk::IStorage;
|
||||
using IRandom = cardboy::sdk::IRandom;
|
||||
using IHighResClock = cardboy::sdk::IHighResClock;
|
||||
using IPowerManager = cardboy::sdk::IPowerManager;
|
||||
using IFilesystem = cardboy::sdk::IFilesystem;
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <buttons.hpp>
|
||||
#include <display.hpp>
|
||||
#include <power_helper.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class PlatformFramebuffer final : public cardboy::sdk::FramebufferFacade<PlatformFramebuffer> {
|
||||
public:
|
||||
[[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
||||
[[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
__attribute__((always_inline)) void drawPixel_impl(int x, int y, bool on) {
|
||||
if (x < 0 || y < 0 || x >= width() || y >= height())
|
||||
return;
|
||||
SMD::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void clear_impl(bool on) {
|
||||
for (int y = 0; y < height(); ++y)
|
||||
for (int x = 0; x < width(); ++x)
|
||||
SMD::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) void frameReady_impl() { SMD::frame_ready(); }
|
||||
__attribute__((always_inline)) void sendFrame_impl(bool clear) { SMD::send_frame(clear); }
|
||||
__attribute__((always_inline)) [[nodiscard]] bool frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); }
|
||||
};
|
||||
|
||||
class PlatformInput final : public cardboy::sdk::InputFacade<PlatformInput> {
|
||||
public:
|
||||
cardboy::sdk::InputState readState_impl() {
|
||||
cardboy::sdk::InputState state{};
|
||||
const uint8_t pressed = Buttons::get().get_pressed();
|
||||
if (pressed & BTN_UP)
|
||||
state.up = true;
|
||||
if (pressed & BTN_LEFT)
|
||||
state.left = true;
|
||||
if (pressed & BTN_RIGHT)
|
||||
state.right = true;
|
||||
if (pressed & BTN_DOWN)
|
||||
state.down = true;
|
||||
if (pressed & BTN_A)
|
||||
state.a = true;
|
||||
if (pressed & BTN_B)
|
||||
state.b = true;
|
||||
if (pressed & BTN_SELECT)
|
||||
state.select = true;
|
||||
if (pressed & BTN_START)
|
||||
state.start = true;
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
class PlatformClock final : public cardboy::sdk::ClockFacade<PlatformClock> {
|
||||
public:
|
||||
std::uint32_t millis_impl() {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void sleep_ms_impl(std::uint32_t ms) {
|
||||
if (ms == 0)
|
||||
return;
|
||||
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
using AppSystem = cardboy::sdk::AppSystem;
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_BAT_MON_HPP
|
||||
#define CB_BAT_MON_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
class BatMon {
|
||||
public:
|
||||
static BatMon& get();
|
||||
float get_voltage() const;
|
||||
float get_charge() const;
|
||||
float get_current() const;
|
||||
|
||||
void pooler(); // FIXME:
|
||||
private:
|
||||
static inline i2c_device_config_t _dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x36,
|
||||
.scl_speed_hz = 100000,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
BatMon();
|
||||
|
||||
volatile float _voltage;
|
||||
volatile float _current;
|
||||
volatile float _charge;
|
||||
|
||||
TaskHandle_t _pooler_task;
|
||||
};
|
||||
|
||||
#endif // CB_BAT_MON_HPP
|
||||
@@ -1,40 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef BUTTONS_HPP
|
||||
#define BUTTONS_HPP
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
typedef enum {
|
||||
BTN_START = 1 << 1,
|
||||
BTN_DOWN = 1 << 6,
|
||||
BTN_SELECT = 1 << 0,
|
||||
BTN_LEFT = 1 << 7,
|
||||
BTN_UP = 1 << 5,
|
||||
BTN_B = 1 << 2,
|
||||
BTN_RIGHT = 1 << 4,
|
||||
BTN_A = 1 << 3,
|
||||
} btn_num;
|
||||
|
||||
class Buttons {
|
||||
public:
|
||||
static Buttons& get();
|
||||
void pooler(); // FIXME:
|
||||
uint8_t get_pressed();
|
||||
void install_isr();
|
||||
void register_listener(TaskHandle_t task);
|
||||
|
||||
TaskHandle_t _pooler_task;
|
||||
|
||||
private:
|
||||
Buttons();
|
||||
|
||||
volatile uint8_t _current;
|
||||
volatile TaskHandle_t _listener = nullptr;
|
||||
};
|
||||
|
||||
|
||||
#endif // BUTTONS_HPP
|
||||
@@ -1,54 +0,0 @@
|
||||
// Simple piezo buzzer helper using LEDC (PWM) for square wave tones.
|
||||
// Provides a tiny queued pattern player for short game SFX without blocking.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class Buzzer {
|
||||
public:
|
||||
static Buzzer &get();
|
||||
|
||||
void init(); // call once from app_main
|
||||
|
||||
// Queue a tone. freq=0 => silence. gap_ms is silence after tone before next.
|
||||
void tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms = 0);
|
||||
|
||||
// Convenience SFX
|
||||
void beepRotate();
|
||||
void beepMove();
|
||||
void beepLock();
|
||||
void beepLines(int lines); // 1..4 lines
|
||||
void beepLevelUp(int level); // after increment
|
||||
void beepGameOver();
|
||||
|
||||
// Mute controls
|
||||
void setMuted(bool m);
|
||||
void toggleMuted();
|
||||
bool isMuted() const { return _muted; }
|
||||
|
||||
// Persistence
|
||||
void loadState();
|
||||
void saveState();
|
||||
|
||||
private:
|
||||
struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; };
|
||||
static constexpr int MAX_QUEUE = 16;
|
||||
Step _queue[MAX_QUEUE]{};
|
||||
int _q_head = 0; // inclusive
|
||||
int _q_tail = 0; // exclusive
|
||||
bool _running = false;
|
||||
bool _in_gap = false;
|
||||
void *_timer = nullptr; // esp_timer_handle_t (opaque here)
|
||||
bool _muted = false;
|
||||
|
||||
Buzzer() = default;
|
||||
void enqueue(const Step &s);
|
||||
bool empty() const { return _q_head == _q_tail; }
|
||||
Step &front() { return _queue[_q_head]; }
|
||||
void popFront();
|
||||
void startNext();
|
||||
void schedule(uint32_t ms, bool gapPhase);
|
||||
void applyFreq(uint32_t freq);
|
||||
static void timerCb(void *arg);
|
||||
void clearQueue() { _q_head = _q_tail = 0; }
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
|
||||
namespace cardboy::backend {
|
||||
using ActiveBackend = EspBackend;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_platform.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
|
||||
namespace cardboy::backend {
|
||||
|
||||
struct EspBackend {
|
||||
using Framebuffer = PlatformFramebuffer;
|
||||
using Input = PlatformInput;
|
||||
using Clock = PlatformClock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend
|
||||
@@ -1,30 +0,0 @@
|
||||
#ifndef CB_CONFIG_HPP
|
||||
#define CB_CONFIG_HPP
|
||||
|
||||
#include "hal/spi_types.h"
|
||||
#include "soc/gpio_num.h"
|
||||
|
||||
#define I2C_SCL GPIO_NUM_8
|
||||
#define I2C_SDA GPIO_NUM_9
|
||||
|
||||
#define SPI_MOSI GPIO_NUM_5
|
||||
#define SPI_MISO GPIO_NUM_0
|
||||
#define SPI_SCK GPIO_NUM_4
|
||||
#define SPI_DISP_CS GPIO_NUM_24
|
||||
#define SPI_DISP_DISP GPIO_NUM_11
|
||||
|
||||
#define SPI_BUS SPI2_HOST
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
|
||||
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
|
||||
|
||||
#define BUZZER_PIN GPIO_NUM_25
|
||||
|
||||
#define PWR_INT GPIO_NUM_10
|
||||
#define PWR_KILL GPIO_NUM_12
|
||||
|
||||
#define EXP_INT GPIO_NUM_1
|
||||
|
||||
#endif
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_DISPLAY_HPP
|
||||
#define CB_DISPLAY_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
// (Async memcpy removed for debugging simplification)
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
|
||||
|
||||
namespace SMD {
|
||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
static constexpr size_t kLineMultiSingle = (kLineBytes + 2);
|
||||
static constexpr size_t kLineDataBytes = kLineMultiSingle * DISP_HEIGHT + 2;
|
||||
|
||||
extern uint8_t* dma_buf;
|
||||
|
||||
void init();
|
||||
// Double-buffered asynchronous frame pipeline:
|
||||
// Usage pattern each frame:
|
||||
// SMD::frame_ready(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
|
||||
// ... write pixels into dma_buf via set_pixel / surface ...
|
||||
// SMD::send_frame(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
|
||||
// // is optionally cleared so the alternate buffer is ready for the next frame
|
||||
void send_frame(bool clear_after_send = true);
|
||||
void frame_ready();
|
||||
bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
|
||||
|
||||
__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
|
||||
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
|
||||
|
||||
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
|
||||
unsigned bitIdx = 1 << (7 - (x % 8)) % 8;
|
||||
|
||||
if (value) {
|
||||
dma_buf[lineIdx] &= ~bitIdx;
|
||||
} else {
|
||||
dma_buf[lineIdx] |= bitIdx;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
|
||||
|
||||
static inline spi_device_interface_config_t _devcfg = {
|
||||
.mode = 0, // SPI mode 0
|
||||
.clock_speed_hz = 10 * 1000 * 1000, // Clock out at 10 MHz
|
||||
.spics_io_num = SPI_DISP_CS, // CS pin
|
||||
.flags = SPI_DEVICE_POSITIVE_CS,
|
||||
.queue_size = 1,
|
||||
.pre_cb = nullptr,
|
||||
.post_cb = s_spi_post_cb,
|
||||
};
|
||||
extern spi_device_handle_t _spi;
|
||||
}; // namespace SMD
|
||||
|
||||
#endif // DISPLAY_HPP
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_err.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
class FsHelper {
|
||||
public:
|
||||
static FsHelper& get();
|
||||
|
||||
esp_err_t mount();
|
||||
void unmount();
|
||||
|
||||
bool isMounted() const { return mounted; }
|
||||
const char* basePath() const { return kBasePath; }
|
||||
const char* partitionLabel() const { return kPartitionLabel; }
|
||||
|
||||
private:
|
||||
FsHelper() = default;
|
||||
|
||||
static constexpr const char* kBasePath = "/lfs";
|
||||
static constexpr const char* kPartitionLabel = "littlefs";
|
||||
static constexpr const bool kFormatOnFailure = true;
|
||||
|
||||
bool mounted = false;
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_I2C_GLOBAL_HPP
|
||||
#define CB_I2C_GLOBAL_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
class I2cGlobal {
|
||||
public:
|
||||
static I2cGlobal& get();
|
||||
i2c_master_bus_handle_t& get_bus_handle();
|
||||
|
||||
private:
|
||||
I2cGlobal();
|
||||
static inline i2c_master_bus_config_t _i2c_mst_config = {
|
||||
.i2c_port = 0,
|
||||
.sda_io_num = I2C_SDA,
|
||||
.scl_io_num = I2C_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags = {.enable_internal_pullup = true, .allow_pd = true},
|
||||
};
|
||||
i2c_master_bus_handle_t _bus_handle;
|
||||
}; // namespace i2c_global
|
||||
|
||||
|
||||
#endif // CB_I2C_GLOBAL_HPP
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
using InputState = cardboy::sdk::InputState;
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 03.03.2025.
|
||||
//
|
||||
|
||||
#ifndef POWER_HELPER_HPP
|
||||
#define POWER_HELPER_HPP
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
|
||||
class PowerHelper {
|
||||
public:
|
||||
static PowerHelper& get();
|
||||
|
||||
bool is_slow() const;
|
||||
void set_slow(bool slow);
|
||||
BaseType_t reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken);
|
||||
void delay(int slow_ms, int normal_ms);
|
||||
|
||||
void install_isr();
|
||||
|
||||
private:
|
||||
PowerHelper();
|
||||
|
||||
bool _slow = false;
|
||||
EventGroupHandle_t _event_group;
|
||||
};
|
||||
|
||||
|
||||
#endif // POWER_HELPER_HPP
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_SHUTDOWNER_HPP
|
||||
#define CB_SHUTDOWNER_HPP
|
||||
|
||||
|
||||
class Shutdowner {
|
||||
public:
|
||||
static Shutdowner& get();
|
||||
void install_isr();
|
||||
void shutdown();
|
||||
private:
|
||||
Shutdowner();
|
||||
};
|
||||
|
||||
|
||||
#endif // CB_SHUTDOWNER_HPP
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_SPI_GLOBAL_HPP
|
||||
#define CB_SPI_GLOBAL_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
class SpiGlobal {
|
||||
public:
|
||||
static SpiGlobal& get();
|
||||
|
||||
private:
|
||||
SpiGlobal();
|
||||
static inline spi_bus_config_t _buscfg = {.mosi_io_num = SPI_MOSI,
|
||||
.miso_io_num = SPI_MISO,
|
||||
.sclk_io_num = SPI_SCK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 12482U};
|
||||
|
||||
}; // namespace spi_global
|
||||
|
||||
|
||||
#endif // CB_SPI_GLOBAL_HPP
|
||||
@@ -1,36 +1,15 @@
|
||||
// Cardboy firmware entry point: boot platform services and run the modular app system.
|
||||
|
||||
#include "app_system.hpp"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_platform.hpp"
|
||||
#include "apps/clock_app.hpp"
|
||||
#include "apps/gameboy_app.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "apps/tetris_app.hpp"
|
||||
#include "config.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <bat_mon.hpp>
|
||||
#include <buttons.hpp>
|
||||
#include <buzzer.hpp>
|
||||
#include <display.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
#include <i2c_global.hpp>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <power_helper.hpp>
|
||||
#include <shutdowner.hpp>
|
||||
#include <spi_global.hpp>
|
||||
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "sdkconfig.h"
|
||||
@@ -69,87 +48,6 @@ constexpr apps::EmbeddedRomDescriptor kEmbeddedRoms[] = {
|
||||
},
|
||||
};
|
||||
|
||||
class EspBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) override {
|
||||
Buzzer::get().tone(freq, duration_ms, gap_ms);
|
||||
}
|
||||
|
||||
void beepRotate() override { Buzzer::get().beepRotate(); }
|
||||
void beepMove() override { Buzzer::get().beepMove(); }
|
||||
void beepLock() override { Buzzer::get().beepLock(); }
|
||||
void beepLines(int lines) override { Buzzer::get().beepLines(lines); }
|
||||
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
|
||||
void beepGameOver() override { Buzzer::get().beepGameOver(); }
|
||||
|
||||
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
|
||||
void toggleMuted() override { Buzzer::get().toggleMuted(); }
|
||||
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
|
||||
};
|
||||
|
||||
class EspBatteryMonitor final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return true; }
|
||||
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
||||
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
||||
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
||||
};
|
||||
|
||||
class EspStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
std::uint32_t value = 0;
|
||||
esp_err_t err = nvs_get_u32(handle, keyStr.c_str(), &value);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK)
|
||||
return false;
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READWRITE, &handle) != ESP_OK)
|
||||
return;
|
||||
nvs_set_u32(handle, keyStr.c_str(), value);
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
};
|
||||
|
||||
class EspRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
||||
};
|
||||
|
||||
class EspHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
||||
};
|
||||
|
||||
class EspPowerManager final : public cardboy::sdk::IPowerManager {
|
||||
public:
|
||||
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
|
||||
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
|
||||
};
|
||||
|
||||
class EspFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
|
||||
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
|
||||
[[nodiscard]] std::string basePath() const override {
|
||||
const char* path = FsHelper::get().basePath();
|
||||
return path ? std::string(path) : std::string{};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||
@@ -308,49 +206,13 @@ extern "C" void app_main() {
|
||||
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||
#endif
|
||||
|
||||
PowerHelper::get();
|
||||
Shutdowner::get();
|
||||
Buttons::get();
|
||||
|
||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
Shutdowner::get().install_isr();
|
||||
PowerHelper::get().install_isr();
|
||||
Buttons::get().install_isr();
|
||||
|
||||
I2cGlobal::get();
|
||||
BatMon::get();
|
||||
SpiGlobal::get();
|
||||
SMD::init();
|
||||
Buzzer::get().init();
|
||||
|
||||
FsHelper::get().mount();
|
||||
|
||||
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
|
||||
|
||||
static PlatformFramebuffer framebuffer;
|
||||
static PlatformInput input;
|
||||
static PlatformClock clock;
|
||||
static cardboy::backend::esp::EspRuntime runtime;
|
||||
|
||||
static EspBuzzer buzzerService;
|
||||
static EspBatteryMonitor batteryService;
|
||||
static EspStorage storageService;
|
||||
static EspRandom randomService;
|
||||
static EspHighResClock highResClockService;
|
||||
static EspPowerManager powerService;
|
||||
static EspFilesystem filesystemService;
|
||||
|
||||
static cardboy::sdk::Services services{};
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResClockService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
|
||||
AppContext context(framebuffer, input, clock);
|
||||
context.services = &services;
|
||||
AppSystem system(context);
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
context.system = &system;
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "bat_mon.hpp"
|
||||
|
||||
#include <power_helper.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
#include "shutdowner.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
|
||||
BatMon& BatMon::get() {
|
||||
static BatMon bat_mon;
|
||||
return bat_mon;
|
||||
}
|
||||
|
||||
static void start_pooler(void* arg) { static_cast<BatMon*>(arg)->pooler(); }
|
||||
|
||||
void WriteRegister(uint8_t reg, uint16_t value) {
|
||||
uint8_t buf2[3];
|
||||
buf2[0] = reg;
|
||||
buf2[1] = value & 0xFF;
|
||||
buf2[2] = value >> 8;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
}
|
||||
|
||||
uint16_t ReadRegister(uint8_t reg) {
|
||||
uint16_t buffer;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 2, -1));
|
||||
return buffer;
|
||||
}
|
||||
void WriteAndVerifyRegister(char RegisterAddress, int RegisterValueToWrite) {
|
||||
int attempt = 0;
|
||||
uint16_t RegisterValueRead;
|
||||
do {
|
||||
WriteRegister(RegisterAddress, RegisterValueToWrite);
|
||||
vTaskDelay(1 / portTICK_PERIOD_MS);
|
||||
RegisterValueRead = ReadRegister(RegisterAddress);
|
||||
} while (RegisterValueToWrite != RegisterValueRead && attempt++ < 3);
|
||||
}
|
||||
|
||||
static constexpr float RSense = 0.1; // 100mOhm
|
||||
static constexpr uint16_t DesignCapMah = 180; // 100mOhm
|
||||
|
||||
constexpr float mahToCap(float mah) { return mah * (1000.0 / 5.0) * RSense; }
|
||||
constexpr float capToMah(uint16_t cap) { return cap * (5.0 / 1000.0) / RSense; }
|
||||
constexpr float regToCurrent(uint16_t reg) {
|
||||
return static_cast<float>(static_cast<int16_t>(reg)) * 0.0015625f / RSense; // Convert to mA
|
||||
}
|
||||
constexpr uint16_t currentToReg(float current) { return static_cast<uint16_t>(current * RSense / 0.0015625f); }
|
||||
constexpr float regToVoltage(uint16_t reg) {
|
||||
return reg * 0.078125f * 0.001f; // Convert to volts
|
||||
}
|
||||
constexpr uint16_t voltageToReg(float voltage) {
|
||||
return static_cast<uint16_t>(voltage / (0.078125f * 0.001f)); // Convert to register value
|
||||
}
|
||||
static constexpr uint16_t DesignCap = mahToCap(DesignCapMah);
|
||||
static constexpr uint16_t IchgTerm = currentToReg(10);
|
||||
static constexpr uint16_t VEmpty = 0b1001011001100001; // (3V/3.88V)
|
||||
static constexpr uint16_t dQAcc = (DesignCap / 32);
|
||||
|
||||
BatMon::BatMon() {
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &_dev_cfg, &dev_handle));
|
||||
|
||||
bool StatusPOR = ReadRegister(0x00) & 0x0002;
|
||||
if (StatusPOR) // POR reset
|
||||
{
|
||||
printf("Gas gauge reset!\n");
|
||||
while (ReadRegister(0x3D) & 1)
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
uint16_t HibCFG = ReadRegister(0xBA); // Store original HibCFG value
|
||||
WriteRegister(0x60, 0x90); // Exit Hibernate Mode step 1
|
||||
WriteRegister(0xBA, 0x0); // Exit Hibernate Mode step 2
|
||||
WriteRegister(0x60, 0x0); // Exit Hibernate Mode step 3
|
||||
WriteRegister(0x18, DesignCap); // Write DesignCap
|
||||
WriteRegister(0x45, DesignCap / 32); // Write dQAcc
|
||||
WriteRegister(0x1E, IchgTerm); // Write IchgTerm
|
||||
WriteRegister(0x3A, VEmpty); // Write VEmpty
|
||||
WriteRegister(0x46, dQAcc * 44138 / DesignCap); // Write dPAcc
|
||||
WriteRegister(0xDB, 0x8000); // Write ModelCFG
|
||||
|
||||
// Poll ModelCFG.Refresh(highest bit), proceed to Step 4 when ModelCFG.Refresh = 0.
|
||||
while (ReadRegister(0xDB) & 0x8000)
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS); // 10ms wait loop. Do not continue until ModelCFG.Refresh == 0.
|
||||
WriteRegister(0xBA, HibCFG); // Restore Original HibCFG value
|
||||
|
||||
uint16_t Status = ReadRegister(0x00); // Read Status
|
||||
WriteAndVerifyRegister(0x00, Status & 0xFFFD); // Write and Verify Status with POR bit cleared
|
||||
}
|
||||
|
||||
xTaskCreate(&start_pooler, "BatMon", 2048, this, tskIDLE_PRIORITY, &_pooler_task);
|
||||
}
|
||||
|
||||
void BatMon::pooler() {
|
||||
while (true) {
|
||||
uint8_t reg = 8;
|
||||
uint16_t buffer;
|
||||
_charge = capToMah(ReadRegister(0x05));
|
||||
_current = regToCurrent(ReadRegister(0x0B));
|
||||
_voltage = regToVoltage(ReadRegister(0x09));
|
||||
PowerHelper::get().delay(10000, 1000);
|
||||
if (_voltage < 3.0f) {
|
||||
Shutdowner::get().shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float BatMon::get_voltage() const { return _voltage; }
|
||||
float BatMon::get_charge() const { return _charge; }
|
||||
float BatMon::get_current() const { return _current; }
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "buttons.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_err.h>
|
||||
#include <power_helper.hpp>
|
||||
#include <rom/ets_sys.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "i2c_global.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
static inline i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x20,
|
||||
.scl_speed_hz = 50000,
|
||||
};
|
||||
|
||||
Buttons& Buttons::get() {
|
||||
static Buttons buttons;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
static void start_pooler(void* arg) { static_cast<Buttons*>(arg)->pooler(); }
|
||||
|
||||
static bool is_on_low;
|
||||
|
||||
static void wakeup(void* arg) {
|
||||
if (is_on_low) {
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_HIGH_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_HIGH_LEVEL));
|
||||
is_on_low = false;
|
||||
|
||||
BaseType_t xResult = pdFAIL;
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
|
||||
xTaskNotifyFromISR(Buttons::get()._pooler_task, 0, eNoAction, &xHigherPriorityTaskWoken);
|
||||
|
||||
PowerHelper::get().reset_slow_isr(&xHigherPriorityTaskWoken);
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
} else {
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
is_on_low = true;
|
||||
}
|
||||
}
|
||||
|
||||
Buttons::Buttons() {
|
||||
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &dev_cfg, &dev_handle));
|
||||
uint8_t buf2[2];
|
||||
|
||||
// Config
|
||||
buf2[0] = 6;
|
||||
buf2[1] = 0xFF;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
buf2[0] = 7;
|
||||
buf2[1] = 0x80;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 2, &_pooler_task);
|
||||
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(EXP_INT));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(EXP_INT, GPIO_MODE_INPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_pull_mode(EXP_INT, GPIO_FLOATING));
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_LOW_LEVEL));
|
||||
is_on_low = true;
|
||||
}
|
||||
|
||||
static void delay(unsigned long long loop) {
|
||||
for (unsigned long long i = 0; i < loop; i++) {
|
||||
asm volatile("nop");
|
||||
}
|
||||
}
|
||||
|
||||
void Buttons::pooler() {
|
||||
while (true) {
|
||||
BaseType_t xResult = xTaskNotifyWait(pdFALSE, ULONG_MAX, nullptr, portMAX_DELAY);
|
||||
uint8_t reg = 0;
|
||||
uint8_t buffer;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
||||
_current = buffer;
|
||||
// read second port too to clear the interrupt
|
||||
reg = 1;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
||||
if (_listener)
|
||||
xTaskNotifyGive(_listener);
|
||||
}
|
||||
}
|
||||
uint8_t Buttons::get_pressed() { return _current; }
|
||||
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
|
||||
|
||||
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
|
||||
@@ -1,191 +0,0 @@
|
||||
// Buzzer implementation
|
||||
#include "buzzer.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <driver/ledc.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_timer.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <nvs.h>
|
||||
|
||||
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
|
||||
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
|
||||
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
|
||||
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
|
||||
|
||||
Buzzer &Buzzer::get() {
|
||||
static Buzzer b;
|
||||
return b;
|
||||
}
|
||||
|
||||
void Buzzer::init() {
|
||||
// Initialize NVS once (safe if already done)
|
||||
static bool nvsInited = false;
|
||||
if (!nvsInited) {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
}
|
||||
nvsInited = true;
|
||||
}
|
||||
ledc_timer_config_t tcfg{};
|
||||
tcfg.speed_mode = LEDC_MODE;
|
||||
tcfg.timer_num = LEDC_TIMER;
|
||||
tcfg.duty_resolution = LEDC_BITS;
|
||||
tcfg.freq_hz = 1000; // placeholder, changed per tone
|
||||
tcfg.clk_cfg = LEDC_AUTO_CLK;
|
||||
ESP_ERROR_CHECK(ledc_timer_config(&tcfg));
|
||||
|
||||
ledc_channel_config_t ccfg{};
|
||||
ccfg.speed_mode = LEDC_MODE;
|
||||
ccfg.channel = LEDC_CH;
|
||||
ccfg.timer_sel = LEDC_TIMER;
|
||||
ccfg.gpio_num = static_cast<int>(BUZZER_PIN);
|
||||
ccfg.duty = 0; // start silent
|
||||
ccfg.hpoint = 0;
|
||||
ccfg.intr_type = LEDC_INTR_DISABLE;
|
||||
ESP_ERROR_CHECK(ledc_channel_config(&ccfg));
|
||||
|
||||
esp_timer_create_args_t args{};
|
||||
args.callback = &Buzzer::timerCb;
|
||||
args.arg = this;
|
||||
args.name = "buzz";
|
||||
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
|
||||
loadState();
|
||||
}
|
||||
|
||||
void Buzzer::applyFreq(uint32_t freq) {
|
||||
if (freq == 0) {
|
||||
ledc_stop(LEDC_MODE, LEDC_CH, 0);
|
||||
return;
|
||||
}
|
||||
ledc_set_freq(LEDC_MODE, LEDC_TIMER, freq);
|
||||
ledc_set_duty(LEDC_MODE, LEDC_CH, (1 << LEDC_BITS) / 2);
|
||||
ledc_update_duty(LEDC_MODE, LEDC_CH);
|
||||
}
|
||||
|
||||
void Buzzer::enqueue(const Step &s) {
|
||||
int nextTail = (_q_tail + 1) % MAX_QUEUE;
|
||||
if (nextTail == _q_head) { // full, drop oldest
|
||||
_q_head = (_q_head + 1) % MAX_QUEUE;
|
||||
}
|
||||
_queue[_q_tail] = s;
|
||||
_q_tail = nextTail;
|
||||
}
|
||||
|
||||
void Buzzer::popFront() {
|
||||
if (!empty())
|
||||
_q_head = (_q_head + 1) % MAX_QUEUE;
|
||||
}
|
||||
|
||||
void Buzzer::startNext() {
|
||||
if (empty()) {
|
||||
_running = false;
|
||||
applyFreq(0);
|
||||
return;
|
||||
}
|
||||
_running = true;
|
||||
_in_gap = false;
|
||||
Step &s = front();
|
||||
applyFreq(s.freq);
|
||||
schedule(s.dur_ms, false);
|
||||
}
|
||||
|
||||
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
|
||||
if (!_timer) return;
|
||||
_in_gap = gapPhase;
|
||||
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
|
||||
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
|
||||
}
|
||||
|
||||
void Buzzer::timerCb(void *arg) {
|
||||
auto *self = static_cast<Buzzer*>(arg);
|
||||
if (!self) return;
|
||||
if (self->_in_gap) {
|
||||
self->popFront();
|
||||
self->startNext();
|
||||
return;
|
||||
}
|
||||
// Tone finished
|
||||
if (!self->empty()) {
|
||||
auto &s = self->front();
|
||||
if (s.gap_ms) {
|
||||
self->applyFreq(0);
|
||||
self->schedule(s.gap_ms, true);
|
||||
return;
|
||||
}
|
||||
self->popFront();
|
||||
self->startNext();
|
||||
}
|
||||
}
|
||||
|
||||
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
|
||||
if (_muted) return; // ignore while muted
|
||||
Step s{freq, duration_ms, gap_ms};
|
||||
enqueue(s);
|
||||
if (!_running)
|
||||
startNext();
|
||||
}
|
||||
|
||||
// ---- Game SFX ----
|
||||
void Buzzer::beepRotate() { tone(1800, 25); }
|
||||
void Buzzer::beepMove() { tone(1200, 12); }
|
||||
void Buzzer::beepLock() { tone(900, 25); }
|
||||
void Buzzer::beepLines(int lines) {
|
||||
static const uint32_t base = 1100;
|
||||
for (int i = 0; i < lines; ++i) {
|
||||
tone(base + i * 190, 40, 12);
|
||||
}
|
||||
}
|
||||
void Buzzer::beepLevelUp(int) {
|
||||
tone(1600, 70, 25);
|
||||
tone(2000, 90, 0);
|
||||
}
|
||||
void Buzzer::beepGameOver() {
|
||||
tone(1000, 140, 40);
|
||||
tone(700, 140, 40);
|
||||
tone(400, 260, 0);
|
||||
}
|
||||
|
||||
void Buzzer::setMuted(bool m) {
|
||||
if (m == _muted) return;
|
||||
_muted = m;
|
||||
if (_muted) {
|
||||
clearQueue();
|
||||
applyFreq(0);
|
||||
if (_timer) {
|
||||
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
|
||||
}
|
||||
_running = false;
|
||||
_in_gap = false;
|
||||
} else {
|
||||
// confirmation chirp
|
||||
tone(1500, 40, 10);
|
||||
tone(1900, 60, 0);
|
||||
}
|
||||
saveState();
|
||||
}
|
||||
|
||||
void Buzzer::toggleMuted() { setMuted(!_muted); }
|
||||
|
||||
void Buzzer::loadState() {
|
||||
nvs_handle_t h;
|
||||
if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) {
|
||||
uint8_t v = 0;
|
||||
if (nvs_get_u8(h, "mute", &v) == ESP_OK) {
|
||||
_muted = (v != 0);
|
||||
}
|
||||
nvs_close(h);
|
||||
}
|
||||
if (_muted) applyFreq(0);
|
||||
}
|
||||
|
||||
void Buzzer::saveState() {
|
||||
nvs_handle_t h;
|
||||
if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) {
|
||||
nvs_set_u8(h, "mute", _muted ? 1 : 0);
|
||||
nvs_commit(h);
|
||||
nvs_close(h);
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
// Double-buffered display implementation with async memcpy ---------------------------------
|
||||
|
||||
#include "display.hpp"
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <driver/gpio.h>
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_async_memcpy.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
DMA_ATTR static uint8_t s_dma_buffer0[SMD::kLineDataBytes]{};
|
||||
DMA_ATTR static uint8_t s_dma_buffer1[SMD::kLineDataBytes]{};
|
||||
static uint8_t* s_dma_buffers[2] = {s_dma_buffer0, s_dma_buffer1};
|
||||
DMA_ATTR static uint8_t dma_buf_template[SMD::kLineDataBytes]{};
|
||||
|
||||
uint8_t* SMD::dma_buf = s_dma_buffers[0];
|
||||
|
||||
spi_device_handle_t SMD::_spi;
|
||||
|
||||
static spi_transaction_t _tx{};
|
||||
static SemaphoreHandle_t _txSem = nullptr;
|
||||
static bool _vcom = false;
|
||||
static TaskHandle_t s_clearTaskHandle = nullptr;
|
||||
static SemaphoreHandle_t s_clearReqSem = nullptr;
|
||||
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
|
||||
static bool s_clearPending[2] = {true, true};
|
||||
|
||||
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
|
||||
// update the maximum data stream supported by underlying DMA engine
|
||||
static async_memcpy_handle_t driver = nullptr;
|
||||
|
||||
static volatile int s_drawBufIdx = 0;
|
||||
|
||||
static unsigned char reverse_bits3(unsigned char b) { return (b * 0x0202020202ULL & 0x010884422010ULL) % 0x3ff; }
|
||||
|
||||
static bool IRAM_ATTR my_async_memcpy_cb(async_memcpy_handle_t /*mcp_hdl*/, async_memcpy_event_t* /*event*/,
|
||||
void* cb_args) {
|
||||
BaseType_t high_task_wakeup = pdFALSE;
|
||||
auto sem = static_cast<SemaphoreHandle_t>(cb_args);
|
||||
xSemaphoreGiveFromISR(sem, &high_task_wakeup);
|
||||
return high_task_wakeup == pdTRUE;
|
||||
}
|
||||
|
||||
extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
|
||||
BaseType_t hpw = pdFALSE;
|
||||
xSemaphoreGiveFromISR(s_clearReqSem, &hpw);
|
||||
if (hpw)
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
static void clear_task(void*) {
|
||||
for (;;) {
|
||||
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
|
||||
spi_transaction_t* r = nullptr;
|
||||
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
|
||||
int bufIdx = (int) r->user;
|
||||
xSemaphoreGive(_txSem);
|
||||
const bool shouldClear = s_clearPending[bufIdx];
|
||||
s_clearPending[bufIdx] = true;
|
||||
if (shouldClear) {
|
||||
constexpr unsigned alignedSize = SMD::kLineDataBytes - (SMD::kLineDataBytes % 4);
|
||||
static_assert(SMD::kLineDataBytes - alignedSize < 8); // Last byte is zero anyway
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, alignedSize,
|
||||
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
|
||||
} else {
|
||||
if (!xSemaphoreGive(s_bufferSem[bufIdx]))
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SMD::init() {
|
||||
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(SPI_DISP_DISP));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(SPI_DISP_DISP, GPIO_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_level(SPI_DISP_DISP, 1));
|
||||
ESP_ERROR_CHECK(gpio_hold_en(SPI_DISP_DISP));
|
||||
for (int buf = 0; buf < 2; ++buf) {
|
||||
auto* fb = s_dma_buffers[buf];
|
||||
for (uint8_t i = 0; i < DISP_HEIGHT; ++i) {
|
||||
fb[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
||||
fb[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
||||
}
|
||||
fb[kLineDataBytes - 1] = 0;
|
||||
}
|
||||
|
||||
s_drawBufIdx = 0;
|
||||
dma_buf = s_dma_buffers[s_drawBufIdx];
|
||||
|
||||
for (int y = 0; y < DISP_HEIGHT; ++y)
|
||||
for (int x = 0; x < DISP_WIDTH; ++x)
|
||||
set_pixel(x, y, false);
|
||||
|
||||
std::memcpy(dma_buf_template, dma_buf, sizeof(dma_buf_template));
|
||||
std::memcpy(s_dma_buffers[1], dma_buf_template, sizeof(dma_buf_template));
|
||||
ESP_ERROR_CHECK(esp_async_memcpy_install(&config, &driver)); // install driver with default DMA engine
|
||||
|
||||
s_clearReqSem = xSemaphoreCreateBinary();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
s_bufferSem[i] = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(s_bufferSem[i]);
|
||||
}
|
||||
|
||||
_txSem = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(_txSem);
|
||||
xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle);
|
||||
}
|
||||
|
||||
bool SMD::frame_transfer_in_flight() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
|
||||
|
||||
void SMD::send_frame(bool clear_after_send) {
|
||||
assert(driver != nullptr);
|
||||
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
|
||||
assert(false);
|
||||
|
||||
const int sendIdx = s_drawBufIdx;
|
||||
assert(sendIdx >= 0 && sendIdx < 2);
|
||||
|
||||
SemaphoreHandle_t sem = s_bufferSem[sendIdx];
|
||||
if (!xSemaphoreTake(sem, 0))
|
||||
assert(false);
|
||||
|
||||
const int nextDrawIdx = sendIdx ^ 1;
|
||||
s_clearPending[sendIdx] = clear_after_send;
|
||||
|
||||
_vcom = !_vcom;
|
||||
_tx = {};
|
||||
_tx.tx_buffer = s_dma_buffers[sendIdx];
|
||||
_tx.length = SMD::kLineDataBytes * 8;
|
||||
_tx.user = (void*) (sendIdx);
|
||||
s_dma_buffers[sendIdx][0] = 0b10000000 | (_vcom << 6);
|
||||
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
||||
|
||||
s_drawBufIdx = nextDrawIdx;
|
||||
dma_buf = s_dma_buffers[nextDrawIdx];
|
||||
}
|
||||
|
||||
void SMD::frame_ready() {
|
||||
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
|
||||
// uint64_t waitedUs = 0;
|
||||
if (!uxSemaphoreGetCount(sem)) {
|
||||
// uint64_t start = esp_timer_get_time();
|
||||
if (!xSemaphoreTake(sem, portMAX_DELAY))
|
||||
assert(false);
|
||||
if (!xSemaphoreGive(sem))
|
||||
assert(false);
|
||||
// waitedUs = esp_timer_get_time() - start;
|
||||
}
|
||||
// if (waitedUs)
|
||||
// printf("Waited %" PRIu64 " us\n", waitedUs);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "fs_helper.hpp"
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_littlefs.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
constexpr const char* kTag = "FsHelper";
|
||||
} // namespace
|
||||
|
||||
FsHelper& FsHelper::get() {
|
||||
static FsHelper instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
esp_err_t FsHelper::mount() {
|
||||
if (mounted)
|
||||
return ESP_OK;
|
||||
|
||||
esp_vfs_littlefs_conf_t conf{};
|
||||
conf.base_path = kBasePath;
|
||||
conf.partition_label = kPartitionLabel;
|
||||
conf.format_if_mount_failed = kFormatOnFailure;
|
||||
conf.dont_mount = false;
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
conf.read_only = false;
|
||||
#endif
|
||||
|
||||
const esp_err_t err = esp_vfs_littlefs_register(&conf);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_NOT_FOUND) {
|
||||
ESP_LOGE(kTag, "Failed to find LittleFS partition '%s'", kPartitionLabel);
|
||||
} else if (err == ESP_FAIL) {
|
||||
ESP_LOGE(kTag, "Failed to mount LittleFS at %s (consider enabling format)",
|
||||
kBasePath);
|
||||
} else {
|
||||
ESP_LOGE(kTag, "esp_vfs_littlefs_register failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
mounted = true;
|
||||
|
||||
size_t total = 0;
|
||||
size_t used = 0;
|
||||
const esp_err_t infoErr = esp_littlefs_info(kPartitionLabel, &total, &used);
|
||||
if (infoErr == ESP_OK) {
|
||||
ESP_LOGI(kTag, "LittleFS mounted at %s (%zu / %zu bytes used)", kBasePath, used, total);
|
||||
} else {
|
||||
ESP_LOGW(kTag, "LittleFS mounted but failed to query usage: %s", esp_err_to_name(infoErr));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void FsHelper::unmount() {
|
||||
if (!mounted)
|
||||
return;
|
||||
const esp_err_t err = esp_vfs_littlefs_unregister(kPartitionLabel);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "Failed to unmount LittleFS (%s)", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
mounted = false;
|
||||
ESP_LOGI(kTag, "LittleFS unmounted from %s", kBasePath);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
|
||||
|
||||
I2cGlobal& I2cGlobal::get() {
|
||||
static I2cGlobal i2cGlobal;
|
||||
return i2cGlobal;
|
||||
}
|
||||
|
||||
I2cGlobal::I2cGlobal() { ESP_ERROR_CHECK(i2c_new_master_bus(&_i2c_mst_config, &_bus_handle)); }
|
||||
|
||||
i2c_master_bus_handle_t& I2cGlobal::get_bus_handle() { return _bus_handle; }
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 03.03.2025.
|
||||
//
|
||||
|
||||
#include "power_helper.hpp"
|
||||
|
||||
#include <config.hpp>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
PowerHelper& PowerHelper::get() {
|
||||
static PowerHelper powerHelper;
|
||||
return powerHelper;
|
||||
}
|
||||
bool PowerHelper::is_slow() const { return _slow; }
|
||||
void PowerHelper::set_slow(bool slow) {
|
||||
_slow = slow;
|
||||
if (_slow) {
|
||||
xEventGroupClearBits(_event_group, 1);
|
||||
} else {
|
||||
xEventGroupSetBits(_event_group, 1);
|
||||
}
|
||||
}
|
||||
|
||||
BaseType_t PowerHelper::reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken) {
|
||||
_slow = false;
|
||||
return xEventGroupSetBitsFromISR(_event_group, 1, xHigherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
static void wakeup(void* arg) {
|
||||
BaseType_t xHigherPriorityTaskWoken, xResult;
|
||||
|
||||
xHigherPriorityTaskWoken = pdFALSE;
|
||||
xResult = static_cast<PowerHelper*>(arg)->reset_slow_isr(&xHigherPriorityTaskWoken);
|
||||
if (xResult != pdFAIL) {
|
||||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||||
};
|
||||
}
|
||||
|
||||
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) { set_slow(false); }
|
||||
|
||||
void PowerHelper::delay(int slow_ms, int normal_ms) {
|
||||
if (is_slow()) {
|
||||
auto cur_ticks = xTaskGetTickCount();
|
||||
TickType_t to_wait = slow_ms / portTICK_PERIOD_MS;
|
||||
TickType_t to_wait_normal = normal_ms / portTICK_PERIOD_MS;
|
||||
auto expected_ticks = cur_ticks + to_wait_normal;
|
||||
xEventGroupWaitBits(_event_group, 1, pdFALSE, pdTRUE, to_wait);
|
||||
auto realTicks = xTaskGetTickCount();
|
||||
if (realTicks < expected_ticks) {
|
||||
vTaskDelay(expected_ticks - realTicks);
|
||||
}
|
||||
} else {
|
||||
vTaskDelay(normal_ms / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
void PowerHelper::install_isr() {
|
||||
// gpio_isr_handler_add(EXP_INT, wakeup, this);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "shutdowner.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
Shutdowner& Shutdowner::get() {
|
||||
static Shutdowner instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR int_shutdown(void* arg) {
|
||||
// printf("Shutting down...\n");
|
||||
ESP_ERROR_CHECK(gpio_hold_dis(PWR_KILL));
|
||||
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 0));
|
||||
}
|
||||
|
||||
void Shutdowner::shutdown() {
|
||||
ESP_ERROR_CHECK(gpio_hold_dis(PWR_KILL));
|
||||
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 0));
|
||||
}
|
||||
|
||||
Shutdowner::Shutdowner() {
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(PWR_KILL));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(PWR_KILL, GPIO_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_level(PWR_KILL, 1));
|
||||
ESP_ERROR_CHECK(gpio_hold_en(PWR_KILL));
|
||||
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(PWR_INT));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(PWR_INT, GPIO_MODE_INPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_pull_mode(PWR_INT, GPIO_FLOATING));
|
||||
ESP_ERROR_CHECK(gpio_set_intr_type(PWR_INT, GPIO_INTR_LOW_LEVEL));
|
||||
// ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||
ESP_ERROR_CHECK(gpio_wakeup_enable(PWR_INT, GPIO_INTR_LOW_LEVEL));
|
||||
// ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
// gpio_isr_handler_add(PWR_INT, shutdown, nullptr);
|
||||
}
|
||||
|
||||
void Shutdowner::install_isr() { gpio_isr_handler_add(PWR_INT, int_shutdown, nullptr); }
|
||||
@@ -1,12 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "spi_global.hpp"
|
||||
|
||||
SpiGlobal& SpiGlobal::get() {
|
||||
static SpiGlobal SpiGlobal;
|
||||
return SpiGlobal;
|
||||
}
|
||||
|
||||
SpiGlobal::SpiGlobal() { ESP_ERROR_CHECK(spi_bus_initialize(SPI_BUS, &_buscfg, SPI_DMA_CH_AUTO)); }
|
||||
Reference in New Issue
Block a user