Compare commits

..

8 Commits

Author SHA1 Message Date
54d5f85538 nice dithering 2025-10-09 23:41:51 +02:00
c3295b9b01 fix rotated text 2025-10-09 23:19:05 +02:00
7fc48e5e93 fixes 2025-10-09 22:56:46 +02:00
afff3d0e02 display fix 2025-10-09 22:25:51 +02:00
0660c40ec4 sdkconfig 2025-10-09 18:57:35 +02:00
8520ef556b better gameboy timings 2025-10-09 18:14:17 +02:00
4e78618556 event loop 2025-10-09 17:12:17 +02:00
49455d1b36 move peanutgb to repo 2025-10-09 16:28:35 +02:00
17 changed files with 4886 additions and 306 deletions

View File

@@ -17,7 +17,7 @@ idf_component_register(SRCS
src/buzzer.cpp
src/fs_helper.cpp
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
INCLUDE_DIRS "include" "Peanut-GB"
INCLUDE_DIRS "include"
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)

View File

@@ -11,6 +11,30 @@
class AppSystem;
using AppTimerHandle = std::uint32_t;
constexpr AppTimerHandle kInvalidAppTimer = 0;
enum class AppEventType {
Button,
Timer,
};
struct AppButtonEvent {
InputState current{};
InputState previous{};
};
struct AppTimerEvent {
AppTimerHandle handle = kInvalidAppTimer;
};
struct AppEvent {
AppEventType type;
std::uint32_t timestamp_ms = 0;
AppButtonEvent button{};
AppTimerEvent timer{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
struct BasicAppContext {
using Framebuffer = FramebufferT;
@@ -40,28 +64,50 @@ struct BasicAppContext {
bool hasPendingAppSwitch() const { return pendingSwitch; }
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat = false) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(delay_ms, repeat);
}
AppTimerHandle scheduleRepeatingTimer(uint32_t interval_ms) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(interval_ms, true);
}
void cancelTimer(AppTimerHandle handle) {
if (!system)
return;
cancelTimerInternal(handle);
}
void cancelAllTimers() {
if (!system)
return;
cancelAllTimersInternal();
}
private:
friend class AppSystem;
bool pendingSwitch = false;
bool pendingSwitchByName = false;
std::size_t pendingAppIndex = 0;
std::string pendingAppName;
AppTimerHandle scheduleTimerInternal(uint32_t delay_ms, bool repeat);
void cancelTimerInternal(AppTimerHandle handle);
void cancelAllTimersInternal();
};
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
struct AppSleepPlan {
uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh
uint32_t normal_ms = 0; // short sleep for responsiveness on input wake
};
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void step() = 0;
virtual AppSleepPlan sleepPlan(uint32_t now) const { return {}; }
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
};
class IAppFactory {

View File

@@ -2,6 +2,7 @@
#include "app_framework.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
@@ -25,9 +26,54 @@ public:
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private:
template<typename FramebufferT, typename InputT, typename ClockT>
friend struct BasicAppContext;
struct TimerRecord {
AppTimerHandle id = kInvalidAppTimer;
std::uint32_t generation = 0;
std::uint32_t due_ms = 0;
std::uint32_t interval_ms = 0;
bool repeat = false;
bool active = false;
};
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat);
void cancelTimer(AppTimerHandle handle);
void cancelAllTimers();
void dispatchEvent(const AppEvent& event);
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::size_t activeIndex = static_cast<std::size_t>(-1);
std::vector<TimerRecord> timers;
AppTimerHandle nextTimerId = 1;
std::uint32_t currentGeneration = 0;
InputState lastInputState{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(uint32_t delay_ms, bool repeat) {
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
}
template<typename FramebufferT, typename InputT, typename ClockT>
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
if (system)
system->cancelTimer(handle);
}
template<typename FramebufferT, typename InputT, typename ClockT>
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
if (system)
system->cancelAllTimers();
}

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ public:
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
void register_listener(TaskHandle_t task);
TaskHandle_t _pooler_task;
@@ -32,6 +33,7 @@ private:
Buttons();
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
};

View File

@@ -21,15 +21,15 @@ 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[SMD::kLineDataBytes];
extern uint8_t* dma_buf;
void init();
// Simplified asynchronous frame pipeline:
// Double-buffered asynchronous frame pipeline:
// Usage pattern each frame:
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer+clear & guarantees pixel area is zeroed
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
// ... write pixels into dma_buf via set_pixel / surface ...
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; when DMA completes it triggers
// // a background clear of pixel bytes for next frame
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
// // is asynchronously cleared so the alternate buffer is ready for the next frame
void async_draw_start();
void async_draw_wait();
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
@@ -50,41 +50,15 @@ static void set_pixel(int x, int y, bool value) {
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 = 6 * 1000 * 1000, // Clock out at 10 MHz
.spics_io_num = SPI_DISP_CS, // CS pin
.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 = 3,
.queue_size = 1,
.pre_cb = nullptr,
.post_cb = s_spi_post_cb,
};
extern spi_device_handle_t _spi;
void ensure_clear_task(); // idempotent; called from init
}; // namespace SMD
class SMDSurface : public Surface<SMDSurface, BwPixel>, public StandardEventQueue<SMDSurface> {
public:
using PixelType = BwPixel;
SMDSurface(EventLoop* loop);
~SMDSurface() override;
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
void clear_impl();
int get_width_impl() const;
int get_height_impl() const;
template<typename T>
EventHandlingResult handle(const T& event) {
return _window->handle(event);
}
EventHandlingResult handle(SurfaceResizeEvent event);
};
#endif // DISPLAY_HPP

View File

@@ -22,7 +22,7 @@ private:
.scl_io_num = I2C_SCL,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags = {.enable_internal_pullup = false, .allow_pd = true},
.flags = {.enable_internal_pullup = true, .allow_pd = true},
};
i2c_master_bus_handle_t _bus_handle;
}; // namespace i2c_global

View File

@@ -20,7 +20,7 @@ private:
.sclk_io_num = SPI_SCK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 400 * 240 * 2};
.max_transfer_sz = 12482U};
}; // namespace spi_global

View File

@@ -20,6 +20,7 @@
#include <shutdowner.hpp>
#include <spi_global.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -29,6 +30,160 @@
#include "esp_sleep.h"
#include "sdkconfig.h"
#include <algorithm>
#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
namespace {
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
constexpr uint32_t kStatsTaskStack = 4096;
constexpr char kStatsTaskName[] = "TaskStats";
struct TaskRuntimeSample {
TaskHandle_t handle;
uint32_t runtime;
};
struct TaskUsageRow {
std::string name;
uint64_t delta;
UBaseType_t priority;
uint32_t stackHighWaterBytes;
bool isIdle;
};
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
if (current >= previous)
return static_cast<uint64_t>(current - previous);
return static_cast<uint64_t>(current) + (static_cast<uint64_t>(UINT32_MAX) - previous) + 1ULL;
}
void task_usage_monitor(void*) {
static constexpr char tag[] = "TaskUsage";
std::vector<TaskRuntimeSample> lastSamples;
uint32_t lastTotal = 0;
vTaskDelay(kStatsWarmupDelay);
while (true) {
vTaskDelay(kStatsTaskDelayTicks);
const UBaseType_t taskCount = uxTaskGetNumberOfTasks();
if (taskCount == 0)
continue;
std::vector<TaskStatus_t> statusBuffer(taskCount);
uint32_t totalRuntime = 0;
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
if (captured == 0)
continue;
statusBuffer.resize(captured);
std::vector<TaskRuntimeSample> currentSamples;
currentSamples.reserve(statusBuffer.size());
if (lastTotal == 0) {
for (const auto& status: statusBuffer) {
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
}
lastSamples = std::move(currentSamples);
lastTotal = totalRuntime;
continue;
}
const uint64_t totalDelta = deltaWithWrap(totalRuntime, lastTotal);
if (totalDelta == 0)
continue;
std::vector<TaskUsageRow> rows;
rows.reserve(statusBuffer.size());
uint64_t idleDelta = 0;
uint64_t activeDelta = 0;
uint64_t accountedDelta = 0;
for (const auto& status: statusBuffer) {
const auto it = std::find_if(lastSamples.begin(), lastSamples.end(), [&](const TaskRuntimeSample& entry) {
return entry.handle == status.xHandle;
});
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
const uint64_t taskDelta = (it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
TaskUsageRow row{
.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
.delta = taskDelta,
.priority = status.uxCurrentPriority,
.stackHighWaterBytes = static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)
};
rows.push_back(std::move(row));
accountedDelta += taskDelta;
if (rows.back().isIdle)
idleDelta += taskDelta;
else
activeDelta += taskDelta;
}
lastSamples = std::move(currentSamples);
lastTotal = totalRuntime;
if (rows.empty())
continue;
std::sort(rows.begin(), rows.end(), [](const TaskUsageRow& a, const TaskUsageRow& b) {
return a.delta > b.delta;
});
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
std::printf("\n[%s] CPU usage over %.1f ms window\n", tag, windowMs);
for (const auto& row: rows) {
if (row.delta == 0 || row.isIdle)
continue;
const double pct = (static_cast<double>(row.delta) * 100.0) / static_cast<double>(totalDelta);
std::printf(" %-16s %6.2f%% (prio=%u stack_free=%lu B)\n", row.name.c_str(), pct, row.priority,
static_cast<unsigned long>(row.stackHighWaterBytes));
}
const double idlePct = (idleDelta * 100.0) / static_cast<double>(totalDelta);
std::printf(" %-16s %6.2f%% (aggregated idle)\n", "<idle>", idlePct);
const uint64_t residual = (accountedDelta >= totalDelta) ? 0ULL : (totalDelta - accountedDelta);
if (residual > 0) {
const double residualPct = (static_cast<double>(residual) * 100.0) / static_cast<double>(totalDelta);
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
}
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag,
(activeDelta * 100.0) / static_cast<double>(totalDelta), idlePct);
std::fflush(stdout);
}
}
void start_task_usage_monitor() {
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr, 0);
}
} // namespace
#else
inline void start_task_usage_monitor() {}
#endif
extern "C" void app_main() {
#ifdef CONFIG_PM_ENABLE
// const esp_pm_config_t pm_config = {
@@ -71,5 +226,7 @@ extern "C" void app_main() {
system.registerApp(apps::createTetrisAppFactory());
system.registerApp(apps::createGameboyAppFactory());
start_task_usage_monitor();
system.run();
}

View File

@@ -1,12 +1,22 @@
#include "app_system.hpp"
#include <power_helper.hpp>
#include <buttons.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <algorithm>
#include <limits>
#include <utility>
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
context.system = this;
namespace {
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
return a.up != b.up || a.down != b.down || a.left != b.left || a.right != b.right || a.a != b.a || a.b != b.b ||
a.select != b.select || a.start != b.start;
}
} // namespace
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
if (!factory)
@@ -28,8 +38,8 @@ bool AppSystem::startAppByIndex(std::size_t index) {
return false;
context.system = this;
auto& factory = factories[index];
auto app = factory->create(context);
auto& factory = factories[index];
auto app = factory->create(context);
if (!app)
return false;
@@ -43,7 +53,9 @@ bool AppSystem::startAppByIndex(std::size_t index) {
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
current = std::move(app);
clearTimersForCurrentApp();
current = std::move(app);
lastInputState = context.input.readState();
current->onStart();
return true;
}
@@ -54,29 +66,46 @@ void AppSystem::run() {
return;
}
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
std::vector<AppEvent> events;
events.reserve(4);
while (true) {
current->step();
if (context.pendingSwitch) {
const bool byName = context.pendingSwitchByName;
const std::size_t reqIndex = context.pendingAppIndex;
const std::string reqName = context.pendingAppName;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
bool switched = false;
if (byName) {
switched = startApp(reqName);
} else {
switched = startAppByIndex(reqIndex);
events.clear();
const std::uint32_t now = context.clock.millis();
processDueTimers(now, events);
const InputState inputNow = context.input.readState();
if (inputsDiffer(inputNow, lastInputState)) {
AppEvent evt{};
evt.type = AppEventType::Button;
evt.timestamp_ms = now;
evt.button.current = inputNow;
evt.button.previous = lastInputState;
events.push_back(evt);
lastInputState = inputNow;
}
for (const auto& evt: events) {
dispatchEvent(evt);
if (handlePendingSwitchRequest()) {
break;
}
if (switched)
continue;
}
const auto now = context.clock.millis();
auto plan = current->sleepPlan(now);
if (plan.slow_ms || plan.normal_ms) {
PowerHelper::get().delay(static_cast<int>(plan.slow_ms), static_cast<int>(plan.normal_ms));
const std::uint32_t waitBase = context.clock.millis();
const std::uint32_t waitMs = nextTimerDueMs(waitBase);
TickType_t waitTicks;
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
waitTicks = portMAX_DELAY;
} else {
waitTicks = pdMS_TO_TICKS(waitMs);
if (waitTicks == 0)
waitTicks = 1;
}
ulTaskNotifyTake(pdTRUE, waitTicks);
}
}
@@ -95,3 +124,111 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
}
return static_cast<std::size_t>(-1);
}
AppTimerHandle AppSystem::scheduleTimer(uint32_t delay_ms, bool repeat) {
if (!current)
return kInvalidAppTimer;
TimerRecord record;
record.id = nextTimerId++;
if (record.id == kInvalidAppTimer)
record.id = nextTimerId++;
record.generation = currentGeneration;
const auto now = context.clock.millis();
record.due_ms = now + delay_ms;
record.interval_ms = repeat ? std::max<std::uint32_t>(1, delay_ms) : 0;
record.repeat = repeat;
record.active = true;
timers.push_back(record);
return record.id;
}
void AppSystem::cancelTimer(AppTimerHandle handle) {
auto* timer = findTimer(handle);
if (timer)
timer->active = false;
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
void AppSystem::cancelAllTimers() {
for (auto& timer: timers) {
if (timer.generation == currentGeneration)
timer.active = false;
}
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
void AppSystem::dispatchEvent(const AppEvent& event) {
if (current)
current->handleEvent(event);
}
void AppSystem::processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents) {
for (auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (static_cast<std::int32_t>(now - timer.due_ms) >= 0) {
AppEvent ev{};
ev.type = AppEventType::Timer;
ev.timestamp_ms = now;
ev.timer.handle = timer.id;
outEvents.push_back(ev);
if (timer.repeat) {
const std::uint32_t interval = timer.interval_ms ? timer.interval_ms : 1;
timer.due_ms = now + interval;
} else {
timer.active = false;
}
}
}
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
std::uint32_t AppSystem::nextTimerDueMs(std::uint32_t now) const {
std::uint32_t minWait = std::numeric_limits<std::uint32_t>::max();
for (const auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (static_cast<std::int32_t>(now - timer.due_ms) >= 0)
return 0;
const std::uint32_t delta = timer.due_ms - now;
if (delta < minWait)
minWait = delta;
}
return minWait;
}
void AppSystem::clearTimersForCurrentApp() {
++currentGeneration;
timers.clear();
}
AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) {
for (auto& timer: timers) {
if (!timer.active || timer.generation != currentGeneration)
continue;
if (timer.id == handle)
return &timer;
}
return nullptr;
}
bool AppSystem::handlePendingSwitchRequest() {
if (!context.pendingSwitch)
return false;
const bool byName = context.pendingSwitchByName;
const std::size_t reqIndex = context.pendingAppIndex;
const std::string reqName = context.pendingAppName;
context.pendingSwitch = false;
context.pendingSwitchByName = false;
context.pendingAppName.clear();
bool switched = false;
if (byName) {
switched = startApp(reqName);
} else {
switched = startAppByIndex(reqIndex);
}
return switched;
}

View File

@@ -39,43 +39,27 @@ public:
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
void onStart() override {
cancelRefreshTimer();
lastSnapshot = {};
dirty = true;
renderIfNeeded(captureTime());
}
void step() override {
const auto snap = captureTime();
InputState st = context.input.readState();
if (st.b && !backPrev) {
context.requestAppSwitchByName(kMenuAppName);
backPrev = st.b;
selectPrev = st.select;
return;
}
if (st.select && !selectPrev) {
use24Hour = !use24Hour;
dirty = true;
}
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
renderIfNeeded(snap);
backPrev = st.b;
selectPrev = st.select;
lastSnapshot = snap;
refreshTimer = context.scheduleRepeatingTimer(200);
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
AppSleepPlan plan;
plan.slow_ms = 200;
plan.normal_ms = 40;
return plan;
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const AppEvent& event) override {
switch (event.type) {
case AppEventType::Button:
handleButtonEvent(event.button);
break;
case AppEventType::Timer:
if (event.timer.handle == refreshTimer)
updateDisplay();
break;
}
}
private:
@@ -83,13 +67,44 @@ private:
Framebuffer& framebuffer;
Clock& clock;
bool use24Hour = true;
bool dirty = false;
bool backPrev = false;
bool selectPrev = false;
bool use24Hour = true;
bool dirty = false;
AppTimerHandle refreshTimer = kInvalidAppTimer;
TimeSnapshot lastSnapshot{};
void cancelRefreshTimer() {
if (refreshTimer != kInvalidAppTimer) {
context.cancelTimer(refreshTimer);
refreshTimer = kInvalidAppTimer;
}
}
void handleButtonEvent(const AppButtonEvent& button) {
const auto& current = button.current;
const auto& previous = button.previous;
if (current.b && !previous.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (current.select && !previous.select) {
use24Hour = !use24Hour;
dirty = true;
}
updateDisplay();
}
void updateDisplay() {
const auto snap = captureTime();
if (!sameSnapshot(snap, lastSnapshot))
dirty = true;
renderIfNeeded(snap);
lastSnapshot = snap;
}
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second;
}

View File

@@ -1,13 +1,14 @@
#pragma GCC optimize("Ofast")
#include "apps/gameboy_app.hpp"
#include "apps/peanut_gb.h"
#include "app_framework.hpp"
#include "app_system.hpp"
#include "font16x8.hpp"
#include <disp_tools.hpp>
#include <fs_helper.hpp>
#include <peanut_gb.h>
#include "esp_timer.h"
@@ -26,6 +27,18 @@
#include <sys/stat.h>
#include <vector>
#define GAMEBOY_PERF_METRICS 0
#ifndef GAMEBOY_PERF_METRICS
#define GAMEBOY_PERF_METRICS 1
#endif
#if GAMEBOY_PERF_METRICS
#define GB_PERF_ONLY(...) __VA_ARGS__
#else
#define GB_PERF_ONLY(...)
#endif
namespace apps {
namespace {
@@ -159,7 +172,9 @@ public:
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
void onStart() override {
perf.resetAll();
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.resetAll();)
prevInput = {};
statusMessage.clear();
resetFpsStats();
@@ -169,37 +184,57 @@ public:
refreshRomList();
mode = Mode::Browse;
browserDirty = true;
scheduleNextTick(0);
}
void onStop() override {
perf.maybePrintAggregate(true);
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
unloadRom();
}
void step() override {
perf.resetForStep();
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
const uint64_t frameStartUs = esp_timer_get_time();
performStep();
const uint64_t frameEndUs = esp_timer_get_time();
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
scheduleAfterFrame(elapsedUs);
return;
}
if (event.type == AppEventType::Button) {
frameDelayCarryUs = 0;
scheduleNextTick(0);
}
}
const uint64_t inputStartUs = esp_timer_get_time();
const InputState input = context.input.readState();
perf.inputUs = esp_timer_get_time() - inputStartUs;
void performStep() {
GB_PERF_ONLY(perf.resetForStep();)
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
const InputState input = context.input.readState();
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
const Mode stepMode = mode;
switch (stepMode) {
case Mode::Browse: {
const uint64_t handleStartUs = esp_timer_get_time();
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
handleBrowserInput(input);
perf.handleUs = esp_timer_get_time() - handleStartUs;
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
const uint64_t renderStartUs = esp_timer_get_time();
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
renderBrowser();
perf.renderUs = esp_timer_get_time() - renderStartUs;
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
break;
}
case Mode::Running: {
const uint64_t handleStartUs = esp_timer_get_time();
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
handleGameInput(input);
perf.handleUs = esp_timer_get_time() - handleStartUs;
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
if (!gbReady) {
mode = Mode::Browse;
@@ -207,41 +242,28 @@ public:
break;
}
const uint64_t geometryStartUs = esp_timer_get_time();
GB_PERF_ONLY(const uint64_t geometryStartUs = esp_timer_get_time();)
ensureRenderGeometry();
perf.geometryUs = esp_timer_get_time() - geometryStartUs;
GB_PERF_ONLY(perf.geometryUs = esp_timer_get_time() - geometryStartUs;)
const uint64_t waitStartUs = esp_timer_get_time();
DispTools::draw_to_display_async_wait();
perf.waitUs = esp_timer_get_time() - waitStartUs;
const uint64_t runStartUs = esp_timer_get_time();
GB_PERF_ONLY(const uint64_t runStartUs = esp_timer_get_time();)
gb_run_frame(&gb);
perf.runUs = esp_timer_get_time() - runStartUs;
GB_PERF_ONLY(perf.runUs = esp_timer_get_time() - runStartUs;)
const uint64_t renderStartUs = esp_timer_get_time();
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
renderGameFrame();
perf.renderUs = esp_timer_get_time() - renderStartUs;
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
break;
}
}
prevInput = input;
perf.finishStep();
perf.accumulate();
perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(), selectedIndex,
browserDirty);
perf.maybePrintAggregate();
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
if (mode == Mode::Running)
return {};
AppSleepPlan plan;
plan.slow_ms = 140;
plan.normal_ms = 50;
return plan;
GB_PERF_ONLY(perf.finishStep();)
GB_PERF_ONLY(perf.accumulate();)
GB_PERF_ONLY(perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(),
selectedIndex, browserDirty);)
GB_PERF_ONLY(perf.maybePrintAggregate();)
}
private:
@@ -451,22 +473,34 @@ private:
class ScopedCallbackTimer {
public:
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) : app(instance), kind(kind) {
if (app)
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) {
#if GAMEBOY_PERF_METRICS
if (instance) {
app = instance;
cbKind = kind;
startUs = esp_timer_get_time();
}
#else
(void) instance;
(void) kind;
#endif
}
~ScopedCallbackTimer() {
#if GAMEBOY_PERF_METRICS
if (!app)
return;
const uint64_t end = esp_timer_get_time();
app->perf.addCallback(kind, end - startUs);
app->perf.addCallback(cbKind, end - startUs);
#endif
}
private:
GameboyApp* app;
PerfTracker::CallbackKind kind;
#if GAMEBOY_PERF_METRICS
GameboyApp* app = nullptr;
PerfTracker::CallbackKind cbKind{};
uint64_t startUs = 0;
#endif
};
struct RenderGeometry {
@@ -484,9 +518,12 @@ private:
std::array<int, LCD_WIDTH> colXEnd{};
};
AppContext& context;
Framebuffer& framebuffer;
PerfTracker perf{};
AppContext& context;
Framebuffer& framebuffer;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::Original;
@@ -505,13 +542,47 @@ private:
const uint8_t* romDataView = nullptr;
std::size_t romDataViewSize = 0;
std::vector<uint8_t> cartRam;
bool frameDirty = false;
uint32_t fpsLastSampleMs = 0;
uint32_t fpsFrameCounter = 0;
uint32_t fpsCurrent = 0;
bool frameDirty = false;
uint32_t fpsLastSampleMs = 0;
uint32_t fpsFrameCounter = 0;
uint32_t totalFrameCounter = 0;
uint32_t fpsCurrent = 0;
std::string activeRomName;
std::string activeRomSavePath;
void cancelTick() {
if (tickTimer != kInvalidAppTimer) {
context.cancelTimer(tickTimer);
tickTimer = kInvalidAppTimer;
}
}
void scheduleNextTick(uint32_t delayMs) {
cancelTick();
tickTimer = context.scheduleTimer(delayMs, false);
}
uint32_t idleDelayMs() const { return browserDirty ? 50 : 140; }
void scheduleAfterFrame(uint64_t elapsedUs) {
if (mode == Mode::Running && gbReady) {
int64_t desiredUs = static_cast<int64_t>(kTargetFrameUs) - static_cast<int64_t>(elapsedUs);
desiredUs += frameDelayCarryUs;
if (desiredUs <= 0) {
frameDelayCarryUs = desiredUs;
scheduleNextTick(0);
return;
}
frameDelayCarryUs = desiredUs % 1000;
desiredUs -= frameDelayCarryUs;
uint32_t delayMs = static_cast<uint32_t>(desiredUs / 1000);
scheduleNextTick(delayMs);
return;
}
frameDelayCarryUs = 0;
scheduleNextTick(idleDelayMs());
}
bool ensureFilesystemReady() {
esp_err_t err = FsHelper::get().mount();
if (err != ESP_OK) {
@@ -866,13 +937,14 @@ private:
return false;
}
gb.direct.priv = this;
gb.direct.joypad = 0xFF;
gb.direct.interlace = false;
gb.direct.frame_skip = false;
gb.direct.priv = this;
gb.direct.joypad = 0xFF;
gb_init_lcd(&gb, &GameboyApp::lcdDrawLine);
gb.direct.interlace = false;
gb.direct.frame_skip = true;
const uint_fast32_t saveSize = gb_get_save_size(&gb);
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
std::string savePath;
@@ -971,6 +1043,7 @@ private:
ensureRenderGeometry();
++fpsFrameCounter;
++totalFrameCounter;
const uint32_t nowMs = context.clock.millis();
if (fpsLastSampleMs == 0)
fpsLastSampleMs = nowMs;
@@ -982,9 +1055,11 @@ private:
fpsLastSampleMs = nowMs;
}
char fpsBuf[16];
std::snprintf(fpsBuf, sizeof(fpsBuf), "%u FPS", static_cast<unsigned>(fpsCurrent));
const std::string fpsText(fpsBuf);
char fpsValueBuf[16];
std::snprintf(fpsValueBuf, sizeof(fpsValueBuf), "%u", static_cast<unsigned>(fpsCurrent));
const std::string fpsValue(fpsValueBuf);
const std::string fpsLabel = "FPS";
const std::string fpsText = fpsValue + " FPS";
const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE";
if (scaleMode == ScaleMode::FullHeight) {
@@ -998,46 +1073,69 @@ private:
const int maxLeftX = std::max(0, screenWidth - rotatedWidth);
const int maxRightXBase = std::max(0, screenWidth - rotatedWidth);
const int horizontalPadding = 8;
const int fpsLineGap = 4;
const int fpsLabelWidth = font16x8::measureText(fpsLabel, 1, 1);
const int fpsValueWidth = font16x8::measureText(fpsValue, 1, 1);
const int fpsBlockWidth = std::max(fpsLabelWidth, fpsValueWidth);
int fpsX = std::max(0, screenWidth - fpsBlockWidth - horizontalPadding);
const int fpsY = horizontalPadding;
font16x8::drawText(framebuffer, fpsX, fpsY, fpsLabel, 1, true, 1);
font16x8::drawText(framebuffer, fpsX, fpsY + font16x8::kGlyphHeight + fpsLineGap, fpsValue, 1, true, 1);
const int reservedTop = fpsY + (font16x8::kGlyphHeight * 2) + fpsLineGap + horizontalPadding;
if (!activeRomName.empty()) {
const int textHeight = measureVerticalText(activeRomName, textScale);
const int maxOrigin = std::max(0, screenHeight - textHeight);
int leftX = std::clamp((leftMargin - rotatedWidth) / 2, 0, maxLeftX);
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
drawTextRotated(framebuffer, leftX, leftY, activeRomName, false, textScale, true, 1);
const std::string rotatedRomName(activeRomName.rbegin(), activeRomName.rend());
const int textHeight = measureVerticalText(rotatedRomName, textScale);
const int maxOrigin = std::max(0, screenHeight - textHeight);
int leftX = 8;
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
drawTextRotated(framebuffer, leftX, leftY, rotatedRomName, true, textScale, true, 1);
if (!statusMessage.empty()) {
const std::string rotatedStatusMessage(statusMessage.rbegin(), statusMessage.rend());
const int textHeight = measureVerticalText(rotatedStatusMessage, textScale);
const int maxOrigin = std::max(0, screenHeight - textHeight);
leftX = leftX + 20;
leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
drawTextRotated(framebuffer, leftX, leftY, rotatedStatusMessage, true, textScale, true, 1);
}
}
const int gap = 8;
int totalRight = 0;
const auto accumulateHeight = [&](std::string_view text) {
if (text.empty())
return;
if (totalRight > 0)
totalRight += gap;
totalRight += measureVerticalText(text, textScale);
};
accumulateHeight(fpsText);
accumulateHeight("START+SELECT BACK");
accumulateHeight(scaleHint);
if (!statusMessage.empty())
accumulateHeight(statusMessage);
const int maxRightOrigin = std::max(0, screenHeight - totalRight);
int rightY = std::clamp((screenHeight - totalRight) / 2, 0, maxRightOrigin);
int rightX = screenWidth - rightMargin + std::max(0, (rightMargin - rotatedWidth) / 2);
rightX = std::clamp(rightX, 0, maxRightXBase);
std::vector<std::string_view> rightTexts;
rightTexts.reserve(2U);
rightTexts.emplace_back("START+SELECT BACK");
rightTexts.emplace_back(scaleHint);
const auto drawRight = [&](std::string_view text) {
const int gap = 8;
int totalRightHeight = 0;
for (std::string_view text: rightTexts) {
if (text.empty())
return;
drawTextRotated(framebuffer, rightX, rightY, text, true, textScale, true, 1);
rightY += measureVerticalText(text, textScale);
rightY += gap;
};
drawRight(fpsText);
drawRight("START+SELECT BACK");
drawRight(scaleHint);
if (!statusMessage.empty())
drawRight(statusMessage);
continue;
std::string rotated(text.rbegin(), text.rend());
if (totalRightHeight > 0)
totalRightHeight += gap;
totalRightHeight += measureVerticalText(rotated, textScale);
}
const int maxRightOrigin = std::max(0, screenHeight - totalRightHeight);
int rightY = std::clamp((screenHeight - totalRightHeight) / 2, 0, maxRightOrigin);
if (rightY < reservedTop)
rightY = std::min(std::max(reservedTop, 0), maxRightOrigin);
int rightX = screenWidth - 20;
for (size_t i = 0; i < rightTexts.size(); ++i) {
std::string_view text = rightTexts[i];
if (text.empty())
continue;
std::string rotated(text.rbegin(), text.rend());
rightY = screenHeight - measureVerticalText(rotated, textScale) - 8;
drawTextRotated(framebuffer, rightX, rightY, rotated, true, textScale, true, 1);
rightX -= 20;
}
} else {
if (!activeRomName.empty())
font16x8::drawText(framebuffer, 16, 16, activeRomName, 1, true, 1);
@@ -1054,8 +1152,8 @@ private:
if (!statusMessage.empty()) {
const int statusWidth = font16x8::measureText(statusMessage, 1, 1);
const int statusX = std::max(12, (framebuffer.width() - statusWidth) / 2);
const int statusY = std::max(16, instructionY - font16x8::kGlyphHeight - 4);
const int statusX = std::max(12, (framebuffer.width() - statusWidth) - 12);
const int statusY = instructionY;
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
}
}
@@ -1127,6 +1225,19 @@ private:
return static_cast<GameboyApp*>(gb->direct.priv);
}
static bool shouldPixelBeOn(int value, int dstX, int dstY, bool useDither) {
value &= 0x03;
if (!useDither)
return value >= 2;
if (value >= 3)
return true;
if (value <= 0)
return false;
constexpr uint8_t pattern[2][2] = {{0, 2}, {3, 1}};
const uint8_t threshold = pattern[dstY & 0x01][dstX & 0x01];
return value > threshold;
}
static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) {
auto* self = fromGb(gb);
if (!self)
@@ -1169,7 +1280,8 @@ private:
self->unloadRom();
}
static void lcdDrawLine(struct gb_s* gb, const uint8_t pixels[160], const uint_fast8_t line) {
__attribute__((optimize("Ofast"))) static void lcdDrawLine(struct gb_s* gb, const uint8_t pixels[160],
const uint_fast8_t line) {
auto* self = fromGb(gb);
if (!self || line >= LCD_HEIGHT)
return;
@@ -1190,11 +1302,19 @@ private:
Framebuffer& fb = self->framebuffer;
GB_PERF_ONLY(const uint64_t waitStartUs = esp_timer_get_time();)
DispTools::draw_to_display_async_wait();
GB_PERF_ONLY(self->perf.waitUs = esp_timer_get_time() - waitStartUs;)
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
const int dstY = yStart;
const int dstXBase = geom.offsetX;
for (int x = 0; x < LCD_WIDTH; ++x) {
const bool on = (pixels[x] & 0x03u) >= 2;
const int val = (pixels[x] & 0x03u);
const bool on = shouldPixelBeOn(val, dstXBase + x, dstY, useDither);
fb.drawPixel(dstXBase + x, dstY, on);
}
return;
@@ -1207,10 +1327,11 @@ private:
const int drawEnd = colEnd[x];
if (drawStart >= drawEnd)
continue;
const bool on = (pixels[x] & 0x03u) >= 2;
for (int dstY = yStart; dstY < yEnd; ++dstY)
for (int dstX = drawStart; dstX < drawEnd; ++dstX)
for (int dstX = drawStart; dstX < drawEnd; ++dstX) {
const bool on = shouldPixelBeOn(pixels[x], dstX, dstY, useDither);
fb.drawPixel(dstX, dstY, on);
}
}
}

View File

@@ -32,45 +32,33 @@ public:
renderIfNeeded();
}
void step() override {
InputState st = context.input.readState();
void handleEvent(const AppEvent& event) override {
if (event.type != AppEventType::Button)
return;
if (st.left && !leftPrev) {
const auto& current = event.button.current;
const auto& previous = event.button.previous;
if (current.left && !previous.left) {
moveSelection(-1);
} else if (st.right && !rightPrev) {
} else if (current.right && !previous.right) {
moveSelection(+1);
}
const bool launch = (st.a && !rotatePrev) || (st.select && !selectPrev);
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
if (launch)
launchSelected();
leftPrev = st.left;
rightPrev = st.right;
rotatePrev = st.a;
selectPrev = st.select;
renderIfNeeded();
}
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
AppSleepPlan plan;
plan.slow_ms = 120;
plan.normal_ms = 40;
return plan;
}
private:
AppContext& context;
Framebuffer& framebuffer;
std::vector<MenuEntry> entries;
std::size_t selected = 0;
bool dirty = false;
bool leftPrev = false;
bool rightPrev = false;
bool rotatePrev = false;
bool selectPrev = false;
bool dirty = false;
void moveSelection(int step) {
if (entries.empty())

View File

@@ -4,6 +4,7 @@
#include "apps/tetris_app.hpp"
#include "app_framework.hpp"
#include "app_system.hpp"
#include "apps/menu_app.hpp"
#include "font16x8.hpp"
@@ -1149,17 +1150,51 @@ namespace {
class TetrisApp final : public IApp {
public:
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
explicit TetrisApp(AppContext& ctx) : context(ctx), game(ctx) {}
void step() override { game.step(); }
void onStart() override {
cancelTick();
scheduleNextTick(0);
}
AppSleepPlan sleepPlan(uint32_t now) const override {
auto plan = game.recommendedSleepMs(now);
return {plan.slow_ms, plan.normal_ms};
void onStop() override { cancelTick(); }
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
runStep();
return;
}
if (event.type == AppEventType::Button) {
scheduleNextTick(0);
}
}
private:
tetris::Game game;
AppContext& context;
tetris::Game game;
AppTimerHandle tickTimer = kInvalidAppTimer;
void runStep() {
game.step();
const auto now = context.clock.millis();
const auto plan = game.recommendedSleepMs(now);
uint32_t delay = plan.normal_ms != 0 ? plan.normal_ms : plan.slow_ms;
scheduleNextTick(delay);
}
void scheduleNextTick(uint32_t delayMs) {
if (tickTimer != kInvalidAppTimer)
context.cancelTimer(tickTimer);
tickTimer = context.scheduleTimer(delayMs, false);
}
void cancelTick() {
if (tickTimer != kInvalidAppTimer) {
context.cancelTimer(tickTimer);
tickTimer = kInvalidAppTimer;
}
}
};
class TetrisAppFactory final : public IAppFactory {

View File

@@ -19,7 +19,7 @@ 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 = 100000,
.scl_speed_hz = 50000,
};
Buttons& Buttons::get() {
@@ -62,7 +62,7 @@ Buttons::Buttons() {
buf2[0] = 7;
buf2[1] = 0x80;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 1, &_pooler_task);
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));
@@ -90,7 +90,11 @@ void Buttons::pooler() {
reg = 1;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, 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; }

View File

@@ -1,46 +1,49 @@
// Simplified display implementation (no async memcpy) ---------------------------------
// Double-buffered display implementation with async memcpy ---------------------------------
#include "display.hpp"
#include <cassert>
#include <cstring>
#include <driver/gpio.h>
#include "disp_tools.hpp"
#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 uint8_t SMD::dma_buf[SMD::kLineDataBytes]{};
DMA_ATTR uint8_t dma_buf_template[SMD::kLineDataBytes]{};
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;
volatile bool _inFlight = false;
static TaskHandle_t s_clearTaskHandle = nullptr;
static SemaphoreHandle_t s_clearReqSem = nullptr;
static SemaphoreHandle_t s_clearSem = nullptr;
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
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 = NULL;
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) {
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;
_inFlight = false;
xSemaphoreGiveFromISR(s_clearSem,
&high_task_wakeup); // high_task_wakeup set to pdTRUE if some high priority task unblocked
auto sem = static_cast<SemaphoreHandle_t>(cb_args);
xSemaphoreGiveFromISR(sem, &high_task_wakeup);
return high_task_wakeup == pdTRUE;
}
static void zero_framebuffer_payload() {
ESP_ERROR_CHECK(esp_async_memcpy(driver, SMD::dma_buf, dma_buf_template, 12480, my_async_memcpy_cb, nullptr));
}
extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
BaseType_t hpw = pdFALSE;
xSemaphoreGiveFromISR(s_clearReqSem, &hpw);
@@ -51,87 +54,92 @@ extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
static void clear_task(void*) {
for (;;) {
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
printf("Started zeroing\n");
spi_transaction_t* r = nullptr;
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
zero_framebuffer_payload();
// printf("Zeroing done\n");
int bufIdx = (int) r->user;
xSemaphoreGive(_txSem);
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, 12480U,
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
}
}
}
void SMD::ensure_clear_task() {
if (!s_clearReqSem)
s_clearReqSem = xSemaphoreCreateBinary();
if (!s_clearSem)
s_clearSem = xSemaphoreCreateBinary();
xSemaphoreGive(s_clearSem);
if (!s_clearTaskHandle)
xTaskCreatePinnedToCore(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle, 0);
}
void SMD::init() {
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
ensure_clear_task();
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 (uint8_t i = 0; i < DISP_HEIGHT; i++) {
dma_buf[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
dma_buf[2 + kLineMultiSingle * i + kLineBytes] = 0;
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;
}
dma_buf[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)
DispTools::set_pixel(x, y, false);
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::async_draw_busy() { return _inFlight; }
bool SMD::async_draw_busy() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
void SMD::async_draw_start() {
assert(!_inFlight);
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
assert(driver != nullptr);
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
assert(false);
_vcom = !_vcom;
_tx = {};
_tx.tx_buffer = dma_buf;
_tx.length = SMD::kLineDataBytes * 8;
dma_buf[0] = 0b10000000 | (_vcom << 6);
_inFlight = true;
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;
_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::async_draw_wait() {
if (!_inFlight || uxSemaphoreGetCount(s_clearSem)) {
// assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight);
return;
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 (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
assert(false);
if (!xSemaphoreGive(s_clearSem))
assert(false);
assert(!_inFlight);
// if (waitedUs)
// printf("Waited %" PRIu64 " us\n", waitedUs);
}
// (clear_in_progress / wait_clear / request_clear removed from public API)
// Surface implementation ------------------------------------------------------
void SMDSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
if (pixel.on)
DispTools::set_pixel(x, y);
else
DispTools::reset_pixel(x, y);
}
void SMDSurface::clear_impl() { DispTools::clear(); }
int SMDSurface::get_width_impl() const { return DISP_WIDTH; }
int SMDSurface::get_height_impl() const { return DISP_HEIGHT; }
EventHandlingResult SMDSurface::handle(SurfaceResizeEvent event) { return _window->handle(event); }
SMDSurface::SMDSurface(EventLoop* loop) :
Surface<SMDSurface, BwPixel>(),
EventQueue<SMDSurface, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>(loop, this) {}
SMDSurface::~SMDSurface() {}

View File

@@ -1699,9 +1699,13 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048
CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10
CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1
# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set
# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set
# CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID is not set
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U32=y
# CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U64 is not set
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set
@@ -1721,6 +1725,7 @@ CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y
CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y
# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set
CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set
# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set
# end of Port