mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
Compare commits
8 Commits
ddf5a47c33
...
54d5f85538
| Author | SHA1 | Date | |
|---|---|---|---|
| 54d5f85538 | |||
| c3295b9b01 | |||
| 7fc48e5e93 | |||
| afff3d0e02 | |||
| 0660c40ec4 | |||
| 8520ef556b | |||
| 4e78618556 | |||
| 49455d1b36 |
@@ -17,7 +17,7 @@ idf_component_register(SRCS
|
|||||||
src/buzzer.cpp
|
src/buzzer.cpp
|
||||||
src/fs_helper.cpp
|
src/fs_helper.cpp
|
||||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
|
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")
|
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
|
||||||
|
|
||||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||||
@@ -11,6 +11,30 @@
|
|||||||
|
|
||||||
class AppSystem;
|
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>
|
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||||
struct BasicAppContext {
|
struct BasicAppContext {
|
||||||
using Framebuffer = FramebufferT;
|
using Framebuffer = FramebufferT;
|
||||||
@@ -40,28 +64,50 @@ struct BasicAppContext {
|
|||||||
|
|
||||||
bool hasPendingAppSwitch() const { return pendingSwitch; }
|
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:
|
private:
|
||||||
friend class AppSystem;
|
friend class AppSystem;
|
||||||
bool pendingSwitch = false;
|
bool pendingSwitch = false;
|
||||||
bool pendingSwitchByName = false;
|
bool pendingSwitchByName = false;
|
||||||
std::size_t pendingAppIndex = 0;
|
std::size_t pendingAppIndex = 0;
|
||||||
std::string pendingAppName;
|
std::string pendingAppName;
|
||||||
|
|
||||||
|
AppTimerHandle scheduleTimerInternal(uint32_t delay_ms, bool repeat);
|
||||||
|
void cancelTimerInternal(AppTimerHandle handle);
|
||||||
|
void cancelAllTimersInternal();
|
||||||
};
|
};
|
||||||
|
|
||||||
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
|
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 {
|
class IApp {
|
||||||
public:
|
public:
|
||||||
virtual ~IApp() = default;
|
virtual ~IApp() = default;
|
||||||
virtual void onStart() {}
|
virtual void onStart() {}
|
||||||
virtual void onStop() {}
|
virtual void onStop() {}
|
||||||
virtual void step() = 0;
|
virtual void handleEvent(const AppEvent& event) = 0;
|
||||||
virtual AppSleepPlan sleepPlan(uint32_t now) const { return {}; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class IAppFactory {
|
class IAppFactory {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "app_framework.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -25,9 +26,54 @@ public:
|
|||||||
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
||||||
|
|
||||||
private:
|
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;
|
AppContext context;
|
||||||
std::vector<std::unique_ptr<IAppFactory>> factories;
|
std::vector<std::unique_ptr<IAppFactory>> factories;
|
||||||
std::unique_ptr<IApp> current;
|
std::unique_ptr<IApp> current;
|
||||||
IAppFactory* activeFactory = nullptr;
|
IAppFactory* activeFactory = nullptr;
|
||||||
std::size_t activeIndex = static_cast<std::size_t>(-1);
|
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();
|
||||||
|
}
|
||||||
|
|||||||
4042
Firmware/main/include/apps/peanut_gb.h
Normal file
4042
Firmware/main/include/apps/peanut_gb.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ public:
|
|||||||
void pooler(); // FIXME:
|
void pooler(); // FIXME:
|
||||||
uint8_t get_pressed();
|
uint8_t get_pressed();
|
||||||
void install_isr();
|
void install_isr();
|
||||||
|
void register_listener(TaskHandle_t task);
|
||||||
|
|
||||||
TaskHandle_t _pooler_task;
|
TaskHandle_t _pooler_task;
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ private:
|
|||||||
Buttons();
|
Buttons();
|
||||||
|
|
||||||
volatile uint8_t _current;
|
volatile uint8_t _current;
|
||||||
|
volatile TaskHandle_t _listener = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
|||||||
static constexpr size_t kLineMultiSingle = (kLineBytes + 2);
|
static constexpr size_t kLineMultiSingle = (kLineBytes + 2);
|
||||||
static constexpr size_t kLineDataBytes = kLineMultiSingle * DISP_HEIGHT + 2;
|
static constexpr size_t kLineDataBytes = kLineMultiSingle * DISP_HEIGHT + 2;
|
||||||
|
|
||||||
extern uint8_t dma_buf[SMD::kLineDataBytes];
|
extern uint8_t* dma_buf;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
// Simplified asynchronous frame pipeline:
|
// Double-buffered asynchronous frame pipeline:
|
||||||
// Usage pattern each frame:
|
// 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 ...
|
// ... 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
|
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
|
||||||
// // a background clear of pixel bytes for next frame
|
// // is asynchronously cleared so the alternate buffer is ready for the next frame
|
||||||
void async_draw_start();
|
void async_draw_start();
|
||||||
void async_draw_wait();
|
void async_draw_wait();
|
||||||
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
|
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
|
||||||
@@ -51,40 +51,14 @@ extern "C" void s_spi_post_cb(spi_transaction_t* trans);
|
|||||||
|
|
||||||
static inline spi_device_interface_config_t _devcfg = {
|
static inline spi_device_interface_config_t _devcfg = {
|
||||||
.mode = 0, // SPI mode 0
|
.mode = 0, // SPI mode 0
|
||||||
.clock_speed_hz = 6 * 1000 * 1000, // Clock out at 10 MHz
|
.clock_speed_hz = 10 * 1000 * 1000, // Clock out at 10 MHz
|
||||||
.spics_io_num = SPI_DISP_CS, // CS pin
|
.spics_io_num = SPI_DISP_CS, // CS pin
|
||||||
.flags = SPI_DEVICE_POSITIVE_CS,
|
.flags = SPI_DEVICE_POSITIVE_CS,
|
||||||
.queue_size = 3,
|
.queue_size = 1,
|
||||||
.pre_cb = nullptr,
|
.pre_cb = nullptr,
|
||||||
.post_cb = s_spi_post_cb,
|
.post_cb = s_spi_post_cb,
|
||||||
};
|
};
|
||||||
extern spi_device_handle_t _spi;
|
extern spi_device_handle_t _spi;
|
||||||
void ensure_clear_task(); // idempotent; called from init
|
|
||||||
}; // namespace SMD
|
}; // 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
|
#endif // DISPLAY_HPP
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ private:
|
|||||||
.scl_io_num = I2C_SCL,
|
.scl_io_num = I2C_SCL,
|
||||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||||
.glitch_ignore_cnt = 7,
|
.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;
|
i2c_master_bus_handle_t _bus_handle;
|
||||||
}; // namespace i2c_global
|
}; // namespace i2c_global
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ private:
|
|||||||
.sclk_io_num = SPI_SCK,
|
.sclk_io_num = SPI_SCK,
|
||||||
.quadwp_io_num = -1,
|
.quadwp_io_num = -1,
|
||||||
.quadhd_io_num = -1,
|
.quadhd_io_num = -1,
|
||||||
.max_transfer_sz = 400 * 240 * 2};
|
.max_transfer_sz = 12482U};
|
||||||
|
|
||||||
}; // namespace spi_global
|
}; // namespace spi_global
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include <shutdowner.hpp>
|
#include <shutdowner.hpp>
|
||||||
#include <spi_global.hpp>
|
#include <spi_global.hpp>
|
||||||
|
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
@@ -29,6 +30,160 @@
|
|||||||
#include "esp_sleep.h"
|
#include "esp_sleep.h"
|
||||||
#include "sdkconfig.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() {
|
extern "C" void app_main() {
|
||||||
#ifdef CONFIG_PM_ENABLE
|
#ifdef CONFIG_PM_ENABLE
|
||||||
// const esp_pm_config_t pm_config = {
|
// const esp_pm_config_t pm_config = {
|
||||||
@@ -71,5 +226,7 @@ extern "C" void app_main() {
|
|||||||
system.registerApp(apps::createTetrisAppFactory());
|
system.registerApp(apps::createTetrisAppFactory());
|
||||||
system.registerApp(apps::createGameboyAppFactory());
|
system.registerApp(apps::createGameboyAppFactory());
|
||||||
|
|
||||||
|
start_task_usage_monitor();
|
||||||
|
|
||||||
system.run();
|
system.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
#include "app_system.hpp"
|
#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>
|
#include <utility>
|
||||||
|
|
||||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
|
namespace {
|
||||||
context.system = this;
|
[[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) {
|
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
||||||
if (!factory)
|
if (!factory)
|
||||||
@@ -43,7 +53,9 @@ bool AppSystem::startAppByIndex(std::size_t index) {
|
|||||||
context.pendingSwitch = false;
|
context.pendingSwitch = false;
|
||||||
context.pendingSwitchByName = false;
|
context.pendingSwitchByName = false;
|
||||||
context.pendingAppName.clear();
|
context.pendingAppName.clear();
|
||||||
|
clearTimersForCurrentApp();
|
||||||
current = std::move(app);
|
current = std::move(app);
|
||||||
|
lastInputState = context.input.readState();
|
||||||
current->onStart();
|
current->onStart();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -54,29 +66,46 @@ void AppSystem::run() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
|
||||||
|
std::vector<AppEvent> events;
|
||||||
|
events.reserve(4);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
current->step();
|
events.clear();
|
||||||
if (context.pendingSwitch) {
|
const std::uint32_t now = context.clock.millis();
|
||||||
const bool byName = context.pendingSwitchByName;
|
processDueTimers(now, events);
|
||||||
const std::size_t reqIndex = context.pendingAppIndex;
|
|
||||||
const std::string reqName = context.pendingAppName;
|
const InputState inputNow = context.input.readState();
|
||||||
context.pendingSwitch = false;
|
if (inputsDiffer(inputNow, lastInputState)) {
|
||||||
context.pendingSwitchByName = false;
|
AppEvent evt{};
|
||||||
context.pendingAppName.clear();
|
evt.type = AppEventType::Button;
|
||||||
bool switched = false;
|
evt.timestamp_ms = now;
|
||||||
if (byName) {
|
evt.button.current = inputNow;
|
||||||
switched = startApp(reqName);
|
evt.button.previous = lastInputState;
|
||||||
|
events.push_back(evt);
|
||||||
|
lastInputState = inputNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& evt: events) {
|
||||||
|
dispatchEvent(evt);
|
||||||
|
if (handlePendingSwitchRequest()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
switched = startAppByIndex(reqIndex);
|
waitTicks = pdMS_TO_TICKS(waitMs);
|
||||||
}
|
if (waitTicks == 0)
|
||||||
if (switched)
|
waitTicks = 1;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ulTaskNotifyTake(pdTRUE, waitTicks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,3 +124,111 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
|
|||||||
}
|
}
|
||||||
return static_cast<std::size_t>(-1);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,43 +39,27 @@ public:
|
|||||||
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
||||||
|
|
||||||
void onStart() override {
|
void onStart() override {
|
||||||
|
cancelRefreshTimer();
|
||||||
lastSnapshot = {};
|
lastSnapshot = {};
|
||||||
dirty = true;
|
dirty = true;
|
||||||
renderIfNeeded(captureTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
void step() override {
|
|
||||||
const auto snap = captureTime();
|
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);
|
renderIfNeeded(snap);
|
||||||
|
|
||||||
backPrev = st.b;
|
|
||||||
selectPrev = st.select;
|
|
||||||
lastSnapshot = snap;
|
lastSnapshot = snap;
|
||||||
|
refreshTimer = context.scheduleRepeatingTimer(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
|
void onStop() override { cancelRefreshTimer(); }
|
||||||
AppSleepPlan plan;
|
|
||||||
plan.slow_ms = 200;
|
void handleEvent(const AppEvent& event) override {
|
||||||
plan.normal_ms = 40;
|
switch (event.type) {
|
||||||
return plan;
|
case AppEventType::Button:
|
||||||
|
handleButtonEvent(event.button);
|
||||||
|
break;
|
||||||
|
case AppEventType::Timer:
|
||||||
|
if (event.timer.handle == refreshTimer)
|
||||||
|
updateDisplay();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -85,11 +69,42 @@ private:
|
|||||||
|
|
||||||
bool use24Hour = true;
|
bool use24Hour = true;
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
bool backPrev = false;
|
AppTimerHandle refreshTimer = kInvalidAppTimer;
|
||||||
bool selectPrev = false;
|
|
||||||
|
|
||||||
TimeSnapshot lastSnapshot{};
|
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) {
|
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;
|
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#pragma GCC optimize("Ofast")
|
#pragma GCC optimize("Ofast")
|
||||||
#include "apps/gameboy_app.hpp"
|
#include "apps/gameboy_app.hpp"
|
||||||
|
#include "apps/peanut_gb.h"
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "app_framework.hpp"
|
||||||
|
#include "app_system.hpp"
|
||||||
#include "font16x8.hpp"
|
#include "font16x8.hpp"
|
||||||
|
|
||||||
#include <disp_tools.hpp>
|
#include <disp_tools.hpp>
|
||||||
#include <fs_helper.hpp>
|
#include <fs_helper.hpp>
|
||||||
|
|
||||||
#include <peanut_gb.h>
|
|
||||||
|
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
|
||||||
@@ -26,6 +27,18 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <vector>
|
#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 apps {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -159,7 +172,9 @@ public:
|
|||||||
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
||||||
|
|
||||||
void onStart() override {
|
void onStart() override {
|
||||||
perf.resetAll();
|
cancelTick();
|
||||||
|
frameDelayCarryUs = 0;
|
||||||
|
GB_PERF_ONLY(perf.resetAll();)
|
||||||
prevInput = {};
|
prevInput = {};
|
||||||
statusMessage.clear();
|
statusMessage.clear();
|
||||||
resetFpsStats();
|
resetFpsStats();
|
||||||
@@ -169,37 +184,57 @@ public:
|
|||||||
refreshRomList();
|
refreshRomList();
|
||||||
mode = Mode::Browse;
|
mode = Mode::Browse;
|
||||||
browserDirty = true;
|
browserDirty = true;
|
||||||
|
scheduleNextTick(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onStop() override {
|
void onStop() override {
|
||||||
perf.maybePrintAggregate(true);
|
cancelTick();
|
||||||
|
frameDelayCarryUs = 0;
|
||||||
|
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
|
||||||
unloadRom();
|
unloadRom();
|
||||||
}
|
}
|
||||||
|
|
||||||
void step() override {
|
void handleEvent(const AppEvent& event) override {
|
||||||
perf.resetForStep();
|
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();
|
void performStep() {
|
||||||
|
GB_PERF_ONLY(perf.resetForStep();)
|
||||||
|
|
||||||
|
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
|
||||||
const InputState input = context.input.readState();
|
const InputState input = context.input.readState();
|
||||||
perf.inputUs = esp_timer_get_time() - inputStartUs;
|
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
|
||||||
|
|
||||||
const Mode stepMode = mode;
|
const Mode stepMode = mode;
|
||||||
|
|
||||||
switch (stepMode) {
|
switch (stepMode) {
|
||||||
case Mode::Browse: {
|
case Mode::Browse: {
|
||||||
const uint64_t handleStartUs = esp_timer_get_time();
|
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
||||||
handleBrowserInput(input);
|
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();
|
renderBrowser();
|
||||||
perf.renderUs = esp_timer_get_time() - renderStartUs;
|
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Mode::Running: {
|
case Mode::Running: {
|
||||||
const uint64_t handleStartUs = esp_timer_get_time();
|
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
||||||
handleGameInput(input);
|
handleGameInput(input);
|
||||||
perf.handleUs = esp_timer_get_time() - handleStartUs;
|
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
||||||
|
|
||||||
if (!gbReady) {
|
if (!gbReady) {
|
||||||
mode = Mode::Browse;
|
mode = Mode::Browse;
|
||||||
@@ -207,41 +242,28 @@ public:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint64_t geometryStartUs = esp_timer_get_time();
|
GB_PERF_ONLY(const uint64_t geometryStartUs = esp_timer_get_time();)
|
||||||
ensureRenderGeometry();
|
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();
|
GB_PERF_ONLY(const uint64_t runStartUs = 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_run_frame(&gb);
|
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();
|
renderGameFrame();
|
||||||
perf.renderUs = esp_timer_get_time() - renderStartUs;
|
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prevInput = input;
|
prevInput = input;
|
||||||
|
|
||||||
perf.finishStep();
|
GB_PERF_ONLY(perf.finishStep();)
|
||||||
perf.accumulate();
|
GB_PERF_ONLY(perf.accumulate();)
|
||||||
perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(), selectedIndex,
|
GB_PERF_ONLY(perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(),
|
||||||
browserDirty);
|
selectedIndex, browserDirty);)
|
||||||
perf.maybePrintAggregate();
|
GB_PERF_ONLY(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -451,22 +473,34 @@ private:
|
|||||||
|
|
||||||
class ScopedCallbackTimer {
|
class ScopedCallbackTimer {
|
||||||
public:
|
public:
|
||||||
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) : app(instance), kind(kind) {
|
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) {
|
||||||
if (app)
|
#if GAMEBOY_PERF_METRICS
|
||||||
|
if (instance) {
|
||||||
|
app = instance;
|
||||||
|
cbKind = kind;
|
||||||
startUs = esp_timer_get_time();
|
startUs = esp_timer_get_time();
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
(void) instance;
|
||||||
|
(void) kind;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
~ScopedCallbackTimer() {
|
~ScopedCallbackTimer() {
|
||||||
|
#if GAMEBOY_PERF_METRICS
|
||||||
if (!app)
|
if (!app)
|
||||||
return;
|
return;
|
||||||
const uint64_t end = esp_timer_get_time();
|
const uint64_t end = esp_timer_get_time();
|
||||||
app->perf.addCallback(kind, end - startUs);
|
app->perf.addCallback(cbKind, end - startUs);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GameboyApp* app;
|
#if GAMEBOY_PERF_METRICS
|
||||||
PerfTracker::CallbackKind kind;
|
GameboyApp* app = nullptr;
|
||||||
|
PerfTracker::CallbackKind cbKind{};
|
||||||
uint64_t startUs = 0;
|
uint64_t startUs = 0;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RenderGeometry {
|
struct RenderGeometry {
|
||||||
@@ -487,6 +521,9 @@ private:
|
|||||||
AppContext& context;
|
AppContext& context;
|
||||||
Framebuffer& framebuffer;
|
Framebuffer& framebuffer;
|
||||||
PerfTracker perf{};
|
PerfTracker perf{};
|
||||||
|
AppTimerHandle tickTimer = kInvalidAppTimer;
|
||||||
|
int64_t frameDelayCarryUs = 0;
|
||||||
|
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
|
||||||
|
|
||||||
Mode mode = Mode::Browse;
|
Mode mode = Mode::Browse;
|
||||||
ScaleMode scaleMode = ScaleMode::Original;
|
ScaleMode scaleMode = ScaleMode::Original;
|
||||||
@@ -508,10 +545,44 @@ private:
|
|||||||
bool frameDirty = false;
|
bool frameDirty = false;
|
||||||
uint32_t fpsLastSampleMs = 0;
|
uint32_t fpsLastSampleMs = 0;
|
||||||
uint32_t fpsFrameCounter = 0;
|
uint32_t fpsFrameCounter = 0;
|
||||||
|
uint32_t totalFrameCounter = 0;
|
||||||
uint32_t fpsCurrent = 0;
|
uint32_t fpsCurrent = 0;
|
||||||
std::string activeRomName;
|
std::string activeRomName;
|
||||||
std::string activeRomSavePath;
|
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() {
|
bool ensureFilesystemReady() {
|
||||||
esp_err_t err = FsHelper::get().mount();
|
esp_err_t err = FsHelper::get().mount();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -868,11 +939,12 @@ private:
|
|||||||
|
|
||||||
gb.direct.priv = this;
|
gb.direct.priv = this;
|
||||||
gb.direct.joypad = 0xFF;
|
gb.direct.joypad = 0xFF;
|
||||||
gb.direct.interlace = false;
|
|
||||||
gb.direct.frame_skip = false;
|
|
||||||
|
|
||||||
gb_init_lcd(&gb, &GameboyApp::lcdDrawLine);
|
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);
|
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
||||||
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
||||||
std::string savePath;
|
std::string savePath;
|
||||||
@@ -971,6 +1043,7 @@ private:
|
|||||||
ensureRenderGeometry();
|
ensureRenderGeometry();
|
||||||
|
|
||||||
++fpsFrameCounter;
|
++fpsFrameCounter;
|
||||||
|
++totalFrameCounter;
|
||||||
const uint32_t nowMs = context.clock.millis();
|
const uint32_t nowMs = context.clock.millis();
|
||||||
if (fpsLastSampleMs == 0)
|
if (fpsLastSampleMs == 0)
|
||||||
fpsLastSampleMs = nowMs;
|
fpsLastSampleMs = nowMs;
|
||||||
@@ -982,9 +1055,11 @@ private:
|
|||||||
fpsLastSampleMs = nowMs;
|
fpsLastSampleMs = nowMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
char fpsBuf[16];
|
char fpsValueBuf[16];
|
||||||
std::snprintf(fpsBuf, sizeof(fpsBuf), "%u FPS", static_cast<unsigned>(fpsCurrent));
|
std::snprintf(fpsValueBuf, sizeof(fpsValueBuf), "%u", static_cast<unsigned>(fpsCurrent));
|
||||||
const std::string fpsText(fpsBuf);
|
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";
|
const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE";
|
||||||
|
|
||||||
if (scaleMode == ScaleMode::FullHeight) {
|
if (scaleMode == ScaleMode::FullHeight) {
|
||||||
@@ -998,46 +1073,69 @@ private:
|
|||||||
const int maxLeftX = std::max(0, screenWidth - rotatedWidth);
|
const int maxLeftX = std::max(0, screenWidth - rotatedWidth);
|
||||||
const int maxRightXBase = 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()) {
|
if (!activeRomName.empty()) {
|
||||||
const int textHeight = measureVerticalText(activeRomName, textScale);
|
const std::string rotatedRomName(activeRomName.rbegin(), activeRomName.rend());
|
||||||
|
const int textHeight = measureVerticalText(rotatedRomName, textScale);
|
||||||
const int maxOrigin = std::max(0, screenHeight - textHeight);
|
const int maxOrigin = std::max(0, screenHeight - textHeight);
|
||||||
int leftX = std::clamp((leftMargin - rotatedWidth) / 2, 0, maxLeftX);
|
int leftX = 8;
|
||||||
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
|
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
|
||||||
drawTextRotated(framebuffer, leftX, leftY, activeRomName, false, textScale, true, 1);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<std::string_view> rightTexts;
|
||||||
|
rightTexts.reserve(2U);
|
||||||
|
rightTexts.emplace_back("START+SELECT BACK");
|
||||||
|
rightTexts.emplace_back(scaleHint);
|
||||||
|
|
||||||
const int gap = 8;
|
const int gap = 8;
|
||||||
int totalRight = 0;
|
int totalRightHeight = 0;
|
||||||
const auto accumulateHeight = [&](std::string_view text) {
|
for (std::string_view text: rightTexts) {
|
||||||
if (text.empty())
|
if (text.empty())
|
||||||
return;
|
continue;
|
||||||
if (totalRight > 0)
|
std::string rotated(text.rbegin(), text.rend());
|
||||||
totalRight += gap;
|
if (totalRightHeight > 0)
|
||||||
totalRight += measureVerticalText(text, textScale);
|
totalRightHeight += gap;
|
||||||
};
|
totalRightHeight += measureVerticalText(rotated, textScale);
|
||||||
accumulateHeight(fpsText);
|
}
|
||||||
accumulateHeight("START+SELECT BACK");
|
|
||||||
accumulateHeight(scaleHint);
|
|
||||||
if (!statusMessage.empty())
|
|
||||||
accumulateHeight(statusMessage);
|
|
||||||
|
|
||||||
const int maxRightOrigin = std::max(0, screenHeight - totalRight);
|
const int maxRightOrigin = std::max(0, screenHeight - totalRightHeight);
|
||||||
int rightY = std::clamp((screenHeight - totalRight) / 2, 0, maxRightOrigin);
|
int rightY = std::clamp((screenHeight - totalRightHeight) / 2, 0, maxRightOrigin);
|
||||||
int rightX = screenWidth - rightMargin + std::max(0, (rightMargin - rotatedWidth) / 2);
|
if (rightY < reservedTop)
|
||||||
rightX = std::clamp(rightX, 0, maxRightXBase);
|
rightY = std::min(std::max(reservedTop, 0), maxRightOrigin);
|
||||||
|
|
||||||
const auto drawRight = [&](std::string_view text) {
|
int rightX = screenWidth - 20;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < rightTexts.size(); ++i) {
|
||||||
|
std::string_view text = rightTexts[i];
|
||||||
if (text.empty())
|
if (text.empty())
|
||||||
return;
|
continue;
|
||||||
drawTextRotated(framebuffer, rightX, rightY, text, true, textScale, true, 1);
|
std::string rotated(text.rbegin(), text.rend());
|
||||||
rightY += measureVerticalText(text, textScale);
|
rightY = screenHeight - measureVerticalText(rotated, textScale) - 8;
|
||||||
rightY += gap;
|
drawTextRotated(framebuffer, rightX, rightY, rotated, true, textScale, true, 1);
|
||||||
};
|
rightX -= 20;
|
||||||
drawRight(fpsText);
|
}
|
||||||
drawRight("START+SELECT BACK");
|
|
||||||
drawRight(scaleHint);
|
|
||||||
if (!statusMessage.empty())
|
|
||||||
drawRight(statusMessage);
|
|
||||||
} else {
|
} else {
|
||||||
if (!activeRomName.empty())
|
if (!activeRomName.empty())
|
||||||
font16x8::drawText(framebuffer, 16, 16, activeRomName, 1, true, 1);
|
font16x8::drawText(framebuffer, 16, 16, activeRomName, 1, true, 1);
|
||||||
@@ -1054,8 +1152,8 @@ private:
|
|||||||
|
|
||||||
if (!statusMessage.empty()) {
|
if (!statusMessage.empty()) {
|
||||||
const int statusWidth = font16x8::measureText(statusMessage, 1, 1);
|
const int statusWidth = font16x8::measureText(statusMessage, 1, 1);
|
||||||
const int statusX = std::max(12, (framebuffer.width() - statusWidth) / 2);
|
const int statusX = std::max(12, (framebuffer.width() - statusWidth) - 12);
|
||||||
const int statusY = std::max(16, instructionY - font16x8::kGlyphHeight - 4);
|
const int statusY = instructionY;
|
||||||
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
|
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1127,6 +1225,19 @@ private:
|
|||||||
return static_cast<GameboyApp*>(gb->direct.priv);
|
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) {
|
static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) {
|
||||||
auto* self = fromGb(gb);
|
auto* self = fromGb(gb);
|
||||||
if (!self)
|
if (!self)
|
||||||
@@ -1169,7 +1280,8 @@ private:
|
|||||||
self->unloadRom();
|
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);
|
auto* self = fromGb(gb);
|
||||||
if (!self || line >= LCD_HEIGHT)
|
if (!self || line >= LCD_HEIGHT)
|
||||||
return;
|
return;
|
||||||
@@ -1190,11 +1302,19 @@ private:
|
|||||||
|
|
||||||
Framebuffer& fb = self->framebuffer;
|
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) {
|
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
|
||||||
const int dstY = yStart;
|
const int dstY = yStart;
|
||||||
const int dstXBase = geom.offsetX;
|
const int dstXBase = geom.offsetX;
|
||||||
for (int x = 0; x < LCD_WIDTH; ++x) {
|
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);
|
fb.drawPixel(dstXBase + x, dstY, on);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -1207,12 +1327,13 @@ private:
|
|||||||
const int drawEnd = colEnd[x];
|
const int drawEnd = colEnd[x];
|
||||||
if (drawStart >= drawEnd)
|
if (drawStart >= drawEnd)
|
||||||
continue;
|
continue;
|
||||||
const bool on = (pixels[x] & 0x03u) >= 2;
|
|
||||||
for (int dstY = yStart; dstY < yEnd; ++dstY)
|
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);
|
fb.drawPixel(dstX, dstY, on);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static const char* initErrorToString(enum gb_init_error_e err) {
|
static const char* initErrorToString(enum gb_init_error_e err) {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
|
|||||||
@@ -32,34 +32,26 @@ public:
|
|||||||
renderIfNeeded();
|
renderIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void step() override {
|
void handleEvent(const AppEvent& event) override {
|
||||||
InputState st = context.input.readState();
|
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);
|
moveSelection(-1);
|
||||||
} else if (st.right && !rightPrev) {
|
} else if (current.right && !previous.right) {
|
||||||
moveSelection(+1);
|
moveSelection(+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool launch = (st.a && !rotatePrev) || (st.select && !selectPrev);
|
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
|
||||||
if (launch)
|
if (launch)
|
||||||
launchSelected();
|
launchSelected();
|
||||||
|
|
||||||
leftPrev = st.left;
|
|
||||||
rightPrev = st.right;
|
|
||||||
rotatePrev = st.a;
|
|
||||||
selectPrev = st.select;
|
|
||||||
|
|
||||||
renderIfNeeded();
|
renderIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
|
|
||||||
AppSleepPlan plan;
|
|
||||||
plan.slow_ms = 120;
|
|
||||||
plan.normal_ms = 40;
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AppContext& context;
|
AppContext& context;
|
||||||
Framebuffer& framebuffer;
|
Framebuffer& framebuffer;
|
||||||
@@ -67,10 +59,6 @@ private:
|
|||||||
std::size_t selected = 0;
|
std::size_t selected = 0;
|
||||||
|
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
bool leftPrev = false;
|
|
||||||
bool rightPrev = false;
|
|
||||||
bool rotatePrev = false;
|
|
||||||
bool selectPrev = false;
|
|
||||||
|
|
||||||
void moveSelection(int step) {
|
void moveSelection(int step) {
|
||||||
if (entries.empty())
|
if (entries.empty())
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "apps/tetris_app.hpp"
|
#include "apps/tetris_app.hpp"
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "app_framework.hpp"
|
||||||
|
#include "app_system.hpp"
|
||||||
#include "apps/menu_app.hpp"
|
#include "apps/menu_app.hpp"
|
||||||
#include "font16x8.hpp"
|
#include "font16x8.hpp"
|
||||||
|
|
||||||
@@ -1149,17 +1150,51 @@ namespace {
|
|||||||
|
|
||||||
class TetrisApp final : public IApp {
|
class TetrisApp final : public IApp {
|
||||||
public:
|
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 {
|
void onStop() override { cancelTick(); }
|
||||||
auto plan = game.recommendedSleepMs(now);
|
|
||||||
return {plan.slow_ms, plan.normal_ms};
|
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:
|
private:
|
||||||
|
AppContext& context;
|
||||||
tetris::Game game;
|
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 {
|
class TetrisAppFactory final : public IAppFactory {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ static i2c_master_dev_handle_t dev_handle;
|
|||||||
static inline i2c_device_config_t dev_cfg = {
|
static inline i2c_device_config_t dev_cfg = {
|
||||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||||
.device_address = 0x20,
|
.device_address = 0x20,
|
||||||
.scl_speed_hz = 100000,
|
.scl_speed_hz = 50000,
|
||||||
};
|
};
|
||||||
|
|
||||||
Buttons& Buttons::get() {
|
Buttons& Buttons::get() {
|
||||||
@@ -62,7 +62,7 @@ Buttons::Buttons() {
|
|||||||
buf2[0] = 7;
|
buf2[0] = 7;
|
||||||
buf2[1] = 0x80;
|
buf2[1] = 0x80;
|
||||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
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_reset_pin(EXP_INT));
|
||||||
ESP_ERROR_CHECK(gpio_set_direction(EXP_INT, GPIO_MODE_INPUT));
|
ESP_ERROR_CHECK(gpio_set_direction(EXP_INT, GPIO_MODE_INPUT));
|
||||||
@@ -90,7 +90,11 @@ void Buttons::pooler() {
|
|||||||
reg = 1;
|
reg = 1;
|
||||||
ESP_ERROR_CHECK(
|
ESP_ERROR_CHECK(
|
||||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
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; }
|
uint8_t Buttons::get_pressed() { return _current; }
|
||||||
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
|
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
|
||||||
|
|
||||||
|
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
|
||||||
|
|||||||
@@ -1,46 +1,49 @@
|
|||||||
// Simplified display implementation (no async memcpy) ---------------------------------
|
// Double-buffered display implementation with async memcpy ---------------------------------
|
||||||
|
|
||||||
#include "display.hpp"
|
#include "display.hpp"
|
||||||
|
#include <cassert>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
#include "disp_tools.hpp"
|
#include "disp_tools.hpp"
|
||||||
#include "driver/spi_master.h"
|
#include "driver/spi_master.h"
|
||||||
#include "esp_async_memcpy.h"
|
#include "esp_async_memcpy.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
DMA_ATTR uint8_t SMD::dma_buf[SMD::kLineDataBytes]{};
|
DMA_ATTR static uint8_t s_dma_buffer0[SMD::kLineDataBytes]{};
|
||||||
DMA_ATTR uint8_t dma_buf_template[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;
|
spi_device_handle_t SMD::_spi;
|
||||||
|
|
||||||
static spi_transaction_t _tx{};
|
static spi_transaction_t _tx{};
|
||||||
|
static SemaphoreHandle_t _txSem = nullptr;
|
||||||
static bool _vcom = false;
|
static bool _vcom = false;
|
||||||
volatile bool _inFlight = false;
|
|
||||||
static TaskHandle_t s_clearTaskHandle = nullptr;
|
static TaskHandle_t s_clearTaskHandle = nullptr;
|
||||||
static SemaphoreHandle_t s_clearReqSem = 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();
|
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
|
||||||
// update the maximum data stream supported by underlying DMA engine
|
// 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 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*/,
|
||||||
static bool IRAM_ATTR my_async_memcpy_cb(async_memcpy_handle_t mcp_hdl, async_memcpy_event_t* event, void* cb_args) {
|
void* cb_args) {
|
||||||
BaseType_t high_task_wakeup = pdFALSE;
|
BaseType_t high_task_wakeup = pdFALSE;
|
||||||
_inFlight = false;
|
auto sem = static_cast<SemaphoreHandle_t>(cb_args);
|
||||||
xSemaphoreGiveFromISR(s_clearSem,
|
xSemaphoreGiveFromISR(sem, &high_task_wakeup);
|
||||||
&high_task_wakeup); // high_task_wakeup set to pdTRUE if some high priority task unblocked
|
|
||||||
return high_task_wakeup == pdTRUE;
|
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*/) {
|
extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
|
||||||
BaseType_t hpw = pdFALSE;
|
BaseType_t hpw = pdFALSE;
|
||||||
xSemaphoreGiveFromISR(s_clearReqSem, &hpw);
|
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*) {
|
static void clear_task(void*) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
|
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
|
||||||
printf("Started zeroing\n");
|
|
||||||
spi_transaction_t* r = nullptr;
|
spi_transaction_t* r = nullptr;
|
||||||
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
|
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
|
||||||
zero_framebuffer_payload();
|
int bufIdx = (int) r->user;
|
||||||
// printf("Zeroing done\n");
|
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() {
|
void SMD::init() {
|
||||||
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
|
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
|
||||||
ensure_clear_task();
|
|
||||||
ESP_ERROR_CHECK(gpio_reset_pin(SPI_DISP_DISP));
|
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_direction(SPI_DISP_DISP, GPIO_MODE_OUTPUT));
|
||||||
ESP_ERROR_CHECK(gpio_set_level(SPI_DISP_DISP, 1));
|
ESP_ERROR_CHECK(gpio_set_level(SPI_DISP_DISP, 1));
|
||||||
ESP_ERROR_CHECK(gpio_hold_en(SPI_DISP_DISP));
|
ESP_ERROR_CHECK(gpio_hold_en(SPI_DISP_DISP));
|
||||||
for (uint8_t i = 0; i < DISP_HEIGHT; i++) {
|
for (int buf = 0; buf < 2; ++buf) {
|
||||||
dma_buf[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
auto* fb = s_dma_buffers[buf];
|
||||||
dma_buf[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
for (uint8_t i = 0; i < DISP_HEIGHT; ++i) {
|
||||||
|
fb[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
||||||
|
fb[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
||||||
}
|
}
|
||||||
dma_buf[kLineDataBytes - 1] = 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 y = 0; y < DISP_HEIGHT; ++y)
|
||||||
for (int x = 0; x < DISP_WIDTH; ++x)
|
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(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
|
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() {
|
void SMD::async_draw_start() {
|
||||||
assert(!_inFlight);
|
assert(driver != nullptr);
|
||||||
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
|
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
|
||||||
assert(false);
|
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;
|
||||||
|
|
||||||
_vcom = !_vcom;
|
_vcom = !_vcom;
|
||||||
_tx = {};
|
_tx = {};
|
||||||
_tx.tx_buffer = dma_buf;
|
_tx.tx_buffer = s_dma_buffers[sendIdx];
|
||||||
_tx.length = SMD::kLineDataBytes * 8;
|
_tx.length = SMD::kLineDataBytes * 8;
|
||||||
dma_buf[0] = 0b10000000 | (_vcom << 6);
|
_tx.user = (void*) (sendIdx);
|
||||||
_inFlight = true;
|
s_dma_buffers[sendIdx][0] = 0b10000000 | (_vcom << 6);
|
||||||
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
||||||
|
|
||||||
|
s_drawBufIdx = nextDrawIdx;
|
||||||
|
dma_buf = s_dma_buffers[nextDrawIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
void SMD::async_draw_wait() {
|
void SMD::async_draw_wait() {
|
||||||
if (!_inFlight || uxSemaphoreGetCount(s_clearSem)) {
|
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
|
||||||
// assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight);
|
// uint64_t waitedUs = 0;
|
||||||
return;
|
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))
|
// if (waitedUs)
|
||||||
assert(false);
|
// printf("Waited %" PRIu64 " us\n", waitedUs);
|
||||||
if (!xSemaphoreGive(s_clearSem))
|
|
||||||
assert(false);
|
|
||||||
assert(!_inFlight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// (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() {}
|
|
||||||
|
|||||||
@@ -1699,9 +1699,13 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048
|
|||||||
CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10
|
CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10
|
||||||
CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
|
CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
|
||||||
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1
|
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_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_USE_TICKLESS_IDLE=y
|
||||||
CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
|
CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
|
||||||
# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set
|
# 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_LVL1=y
|
||||||
# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set
|
# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set
|
||||||
CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y
|
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_PLACE_FUNCTIONS_INTO_FLASH is not set
|
||||||
# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set
|
# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set
|
||||||
# end of Port
|
# end of Port
|
||||||
|
|||||||
Reference in New Issue
Block a user