From ec12561864cd6d18a794c6e74584a22815114ba6 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 01:35:08 +0200 Subject: [PATCH 01/10] lib: rewrite buzzer to play melodies in FreeRTOS task --- sdk/lib/lilka/src/lilka/buzzer.cpp | 80 +++++++++++++++++++++++------- sdk/lib/lilka/src/lilka/buzzer.h | 30 ++++++++--- 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/sdk/lib/lilka/src/lilka/buzzer.cpp b/sdk/lib/lilka/src/lilka/buzzer.cpp index e275fd5b..ca5d48c0 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.cpp +++ b/sdk/lib/lilka/src/lilka/buzzer.cpp @@ -6,58 +6,102 @@ namespace lilka { +Buzzer::Buzzer() : + buzzerMutex(xSemaphoreCreateMutex()), + melodyTaskHandle(NULL), + currentMelody(NULL), + currentMelodyLength(0), + currentMelodyTempo(0) { +} + void Buzzer::begin() { // TODO: Use ledc? #if LILKA_VERSION < 2 serial_err("Buzzer is not supported on this board"); return; -#endif +#else pinMode(LILKA_BUZZER, OUTPUT); +#endif } void Buzzer::play(uint16_t frequency) { #if LILKA_VERSION < 2 serial_err("Buzzer is not supported on this board"); return; -#endif +#else + xSemaphoreTake(buzzerMutex, portMAX_DELAY); + _stop(); tone(LILKA_BUZZER, frequency); + xSemaphoreGive(buzzerMutex); +#endif } void Buzzer::play(uint16_t frequency, uint32_t duration) { #if LILKA_VERSION < 2 serial_err("Buzzer is not supported on this board"); return; -#endif +#else + xSemaphoreTake(buzzerMutex, portMAX_DELAY); + _stop(); tone(LILKA_BUZZER, frequency, duration); + xSemaphoreGive(buzzerMutex); +#endif } -void Buzzer::stop() { +void Buzzer::playMelody(const Tone* melody, uint32_t length, uint32_t tempo) { #if LILKA_VERSION < 2 serial_err("Buzzer is not supported on this board"); - return; +#else + xSemaphoreTake(buzzerMutex, portMAX_DELAY); + _stop(); + currentMelody = static_cast(realloc(currentMelody, length * sizeof(Tone))); + memcpy(currentMelody, melody, length * sizeof(Tone)); + currentMelodyLength = length; + currentMelodyTempo = tempo; + xTaskCreate(melodyTask, "melodyTask", 2048, this, 1, &melodyTaskHandle); + xSemaphoreGive(buzzerMutex); #endif - noTone(LILKA_BUZZER); } -void Buzzer::playMelody(const Tone* melody, uint32_t length, uint32_t tempo) { - // TODO: Make this a FreeRTOS task - for (uint32_t i = 0; i < length; i++) { - Tone tone = melody[i]; - if (tone.size == 0) { +void Buzzer::melodyTask(void* arg) { + Buzzer* buzzer = static_cast(arg); + for (uint32_t i = 0; i < buzzer->currentMelodyLength; i++) { + xSemaphoreTake(buzzer->buzzerMutex, portMAX_DELAY); + Tone currentTone = buzzer->currentMelody[i]; + if (currentTone.size == 0) { + taskYIELD(); continue; } - uint32_t duration = (60000 / tempo) / abs(tone.size); - if (tone.size < 0) { + uint32_t duration = (60000 / buzzer->currentMelodyTempo) / abs(currentTone.size); + if (currentTone.size < 0) { duration += duration / 2; } - if (melody[i].frequency == 0) { - delay(duration); - } else { - play(melody[i].frequency, duration); - delay(duration); + if (buzzer->currentMelody[i].frequency != 0) { + tone(LILKA_BUZZER, buzzer->currentMelody[i].frequency); } + xSemaphoreGive(buzzer->buzzerMutex); + vTaskDelay(duration / portTICK_PERIOD_MS); } + buzzer->stop(); +} + +void Buzzer::stop() { +#if LILKA_VERSION < 2 + serial_err("Buzzer is not supported on this board"); +#else + xSemaphoreTake(buzzerMutex, portMAX_DELAY); + _stop(); + xSemaphoreGive(buzzerMutex); +#endif +} + +void Buzzer::_stop() { + if (melodyTaskHandle != NULL) { + vTaskDelete(melodyTaskHandle); + melodyTaskHandle = NULL; + } + noTone(LILKA_BUZZER); } void Buzzer::playDoom() { diff --git a/sdk/lib/lilka/src/lilka/buzzer.h b/sdk/lib/lilka/src/lilka/buzzer.h index ffbc9e5b..8f6bdd26 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.h +++ b/sdk/lib/lilka/src/lilka/buzzer.h @@ -2,6 +2,9 @@ #define LILKA_BUZZER_H #include +#include +#include +#include namespace lilka { @@ -46,6 +49,10 @@ typedef struct { /// Клас для роботи з п'єзо-динаміком. /// Використовується для відтворення монотонних звуків. /// +/// Всі методи цього класу є неблокуючими, тобто вони не чекають завершення відтворення звуку і не блокують виконання коду, що йде після них. +/// +/// Щоб зупинити відтворення звуку, використовуйте метод `stop()`. +/// /// Приклад використання: /// /// @code @@ -64,24 +71,33 @@ typedef struct { /// @endcode class Buzzer { public: + Buzzer(); /// Почати роботу з п'єзо-динаміком. /// \warning Цей метод викликається автоматично при виклику `lilka::begin()`. void begin(); /// Відтворити ноту з певною частотою. void play(uint16_t frequency); - /// Відтворити ноту з певною частотою протягом певного часу. + /// Відтворити ноту з певною частотою впродовж певного часу. void play(uint16_t frequency, uint32_t duration); - /// Зупинити відтворення. - void stop(); /// Відтворити мелодію. void playMelody(const Tone* melody, uint32_t length, uint32_t tempo = 120); + /// Зупинити відтворення всіх звуків. + void stop(); /// Відтворити мелодію з DOOM - E1M1, At Doom's Gate (Bobby Prince). void playDoom(); -}; -#define DOOM_MELODY -#define DOOM_LENGTH -#define DOOM_TEMPO + static void melodyTask(void* arg); + +private: + // cppcheck-suppress unusedPrivateFunction + void _stop(); + + SemaphoreHandle_t buzzerMutex; + TaskHandle_t melodyTaskHandle; + Tone* currentMelody; + uint32_t currentMelodyLength; + uint32_t currentMelodyTempo; +}; /// Екземпляр класу `Buzzer`, який можна використовувати для відтворення монотонних звуків. /// Вам не потрібно інстанціювати `Buzzer` вручну. From 20d831985e39bbf65a4b7366a038d3a719d097e0 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 01:42:59 +0200 Subject: [PATCH 02/10] keira: add onSuspend / onResume / onStop methods to App class --- firmware/keira/src/app.cpp | 3 +++ firmware/keira/src/app.h | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/firmware/keira/src/app.cpp b/firmware/keira/src/app.cpp index bd4af4c0..3e0dab3e 100644 --- a/firmware/keira/src/app.cpp +++ b/firmware/keira/src/app.cpp @@ -36,17 +36,20 @@ void App::_run(void* data) { void App::suspend() { // TODO: Check if the task is already suspended Serial.println("Suspending app " + String(name) + " (state = " + String(getState()) + ")"); + onSuspend(); vTaskSuspend(taskHandle); } void App::resume() { // TODO: Check if the task is already running Serial.println("Resuming app " + String(name) + " (state = " + String(getState()) + ")"); + onResume(); vTaskResume(taskHandle); } void App::stop() { Serial.println("Stopping app " + String(name) + " (state = " + String(getState()) + ")"); + onStop(); vTaskDelete(taskHandle); } diff --git a/firmware/keira/src/app.h b/firmware/keira/src/app.h index 1ebae756..649fb061 100644 --- a/firmware/keira/src/app.h +++ b/firmware/keira/src/app.h @@ -43,6 +43,12 @@ class App { private: virtual void run() = 0; + virtual void onSuspend() { + } + virtual void onResume() { + } + virtual void onStop() { + } const char* name; uint16_t x, y, w, h; From f6182f9e72cd0203e2df7d36eb6a95015193e0c2 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 14:31:11 +0200 Subject: [PATCH 03/10] lib: fix buzzer not stopping after melody ends\nkeira: fix incorrect pointer cast --- firmware/keira/src/apps/lua/lualilka_display.cpp | 2 +- sdk/lib/lilka/src/lilka/buzzer.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/firmware/keira/src/apps/lua/lualilka_display.cpp b/firmware/keira/src/apps/lua/lualilka_display.cpp index 8569d14f..4a767e84 100644 --- a/firmware/keira/src/apps/lua/lualilka_display.cpp +++ b/firmware/keira/src/apps/lua/lualilka_display.cpp @@ -255,7 +255,7 @@ int lualilka_display_drawImage(lua_State* L) { int lualilka_display_queueDraw(lua_State* L) { // Get App from registry lua_getfield(L, LUA_REGISTRYINDEX, "app"); - App* app = static_cast(app); + App* app = static_cast(lua_touserdata(L, -1)); lua_pop(L, 1); // Queue draw app->queueDraw(); diff --git a/sdk/lib/lilka/src/lilka/buzzer.cpp b/sdk/lib/lilka/src/lilka/buzzer.cpp index ca5d48c0..cdaff2fd 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.cpp +++ b/sdk/lib/lilka/src/lilka/buzzer.cpp @@ -82,6 +82,9 @@ void Buzzer::melodyTask(void* arg) { } xSemaphoreGive(buzzer->buzzerMutex); vTaskDelay(duration / portTICK_PERIOD_MS); + if (i == buzzer->currentMelodyLength - 1) { + noTone(LILKA_BUZZER); + } } buzzer->stop(); } From 155709a0b802f1ffc40d58d7cba8a6ec6939aca0 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 16:54:23 +0200 Subject: [PATCH 04/10] keira: add Lua garbage collection keira: add buzzer sounds to asteroids lib: allocate images in PSRAM buzzer: fix deadlocks --- firmware/keira/sdcard/asteroids/asteroids.lua | 64 ++++++++++++++++++ firmware/keira/src/app.cpp | 2 +- .../keira/src/apps/lua/lualilka_buzzer.cpp | 66 +++++++++++++++++++ firmware/keira/src/apps/lua/lualilka_buzzer.h | 6 ++ .../keira/src/apps/lua/lualilka_display.cpp | 1 + firmware/keira/src/apps/lua/luarunner.cpp | 14 ++-- sdk/lib/lilka/src/lilka/buzzer.cpp | 26 +++++--- sdk/lib/lilka/src/lilka/display.cpp | 3 +- 8 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 firmware/keira/src/apps/lua/lualilka_buzzer.cpp create mode 100644 firmware/keira/src/apps/lua/lualilka_buzzer.h diff --git a/firmware/keira/sdcard/asteroids/asteroids.lua b/firmware/keira/sdcard/asteroids/asteroids.lua index 29fe4aeb..681fc7b0 100644 --- a/firmware/keira/sdcard/asteroids/asteroids.lua +++ b/firmware/keira/sdcard/asteroids/asteroids.lua @@ -68,6 +68,67 @@ end PRESS_START = resources.load_image(ROOT .. "press_start.bmp") YOU_ARE_DEAD = resources.load_image(ROOT .. "game_over.bmp") +-- Buzzer sound for shooting. +-- First element of each item is frequency, second is note size. +SHOOT_SOUND = { + {880, 8}, + {784, 8}, + {698, 8}, + {659, 8}, + {587, 8}, + {523, 8}, + {440, 8}, + {392, 8}, + {349, 8}, + {330, 8}, + {294, 8}, + {262, 8}, +} + +BOOM_SOUND = { + { 440, 8 }, + { 392, 8 }, + { 349, 8 }, + { 330, 8 }, + { 294, 8 }, + { 262, 8 }, + { 220, 8 }, + { 196, 8 }, + { 175, 8 }, + { 165, 8 }, + { 147, 8 }, + { 131, 8 }, + { 123, 8 }, + { 110, 8 }, + { 98, 8 }, + { 88, 8 }, +} + +-- Low-pitch (100-200 Hz) noise. +DEATH_SOUND = { + { 200, 8 }, + { 100, 8 }, + { 150, 8 }, + { 200, 8 }, + { 100, 8 }, + { 150, 8 }, + { 100, 8 }, + { 150, 8 }, + { 100, 8 }, + { 150, 8 }, + { 50, 8 }, + { 100, 8 }, + { 50, 8 }, + { 100, 8 }, + { 50, 8 }, + { 100, 8 }, + { 50, 8 }, + { 100, 8 }, + { 50, 8 }, + { 100, 8 }, + { 50, 8 }, +} + ------------------------------------------------------------------------------- -- Ігрові класи ------------------------------------------------------------------------------- @@ -397,6 +458,7 @@ function lilka.update(delta) bullet.speed_x = ship.speed_x / 2 + display.width * dir_x bullet.speed_y = ship.speed_y / 2 + display.width * dir_y table.insert(bullets, bullet) + buzzer.play_melody(SHOOT_SOUND, 400) end -- Вихід з гри @@ -443,6 +505,7 @@ function lilka.update(delta) -- Якщо куля влучила в астероїд, то вони обидвоє вмирають asteroid:kill() bullet:kill() + buzzer.play_melody(BOOM_SOUND, 400) end end if math.dist(asteroid.x, asteroid.y, ship.x, ship.y) < (asteroid.sprite.width / 3 + ship.width / 2) then @@ -452,6 +515,7 @@ function lilka.update(delta) asteroid:kill() ship:kill() game_state = STATES.GAME_OVER + buzzer.play_melody(DEATH_SOUND, 400) end end end diff --git a/firmware/keira/src/app.cpp b/firmware/keira/src/app.cpp index 3e0dab3e..59b522c0 100644 --- a/firmware/keira/src/app.cpp +++ b/firmware/keira/src/app.cpp @@ -21,7 +21,7 @@ App::App(const char* name, uint16_t x, uint16_t y, uint16_t w, uint16_t h) : nam void App::start() { Serial.println("Starting app " + String(name)); - xTaskCreate(_run, name, 32768, this, 1, &taskHandle); + xTaskCreate(_run, name, 8192, this, 1, &taskHandle); } void App::_run(void* data) { diff --git a/firmware/keira/src/apps/lua/lualilka_buzzer.cpp b/firmware/keira/src/apps/lua/lualilka_buzzer.cpp new file mode 100644 index 00000000..11c7062b --- /dev/null +++ b/firmware/keira/src/apps/lua/lualilka_buzzer.cpp @@ -0,0 +1,66 @@ +#include + +#include "lualilka_buzzer.h" + +extern jmp_buf stopjmp; + +int lualilka_buzzer_play(lua_State* L) { + // Takes 1 or 2 args: frequency or frequency and duration + int n = lua_gettop(L); + if (n < 1 || n > 2) { + return luaL_error(L, "Очікується 1 або 2 аргументи, отримано %d", n); + } + int freq = luaL_checkinteger(L, 1); + if (n == 1) { + lilka::buzzer.play(freq); + } else { + int duration = luaL_checkinteger(L, 2); + lilka::buzzer.play(freq, duration); + } + return 0; +} + +int lualilka_buzzer_playMelody(lua_State* L) { + // Takes 2 arg: table of tones and tempo. Each tone is a table of 2 elements: frequency and duration + // Convert them to array of arrays + int n = lua_gettop(L); + if (n != 2) { + return luaL_error(L, "Очікується 2 аргументи, отримано %d", n); + } + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, 2, LUA_TNUMBER); + int size = luaL_len(L, 1); + int tempo = luaL_checkinteger(L, 2); + lilka::Tone melody[size]; + for (int i = 0; i < size; i++) { + lua_geti(L, 1, i + 1); + luaL_checktype(L, -1, LUA_TTABLE); + lua_rawgeti(L, -1, 1); + lua_rawgeti(L, -2, 2); + melody[i].frequency = luaL_checkinteger(L, -2); + melody[i].size = luaL_checkinteger(L, -1); + lua_pop(L, 3); + } + lua_pop(L, 2); + lilka::buzzer.playMelody(melody, size, tempo); + return 0; +} + +int lualilka_buzzer_stop(lua_State* L) { + lilka::buzzer.stop(); + return 0; +} + +static const luaL_Reg lualilka_buzzer[] = { + {"play", lualilka_buzzer_play}, + {"play_melody", lualilka_buzzer_playMelody}, + {"stop", lualilka_buzzer_stop}, + {NULL, NULL}, +}; + +int lualilka_buzzer_register(lua_State* L) { + // Create global "buzzer" table that contains all buzzer functions + luaL_newlib(L, lualilka_buzzer); + lua_setglobal(L, "buzzer"); + return 0; +} diff --git a/firmware/keira/src/apps/lua/lualilka_buzzer.h b/firmware/keira/src/apps/lua/lualilka_buzzer.h new file mode 100644 index 00000000..d739a900 --- /dev/null +++ b/firmware/keira/src/apps/lua/lualilka_buzzer.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +int lualilka_buzzer_register(lua_State* L); diff --git a/firmware/keira/src/apps/lua/lualilka_display.cpp b/firmware/keira/src/apps/lua/lualilka_display.cpp index 4a767e84..1f1a6120 100644 --- a/firmware/keira/src/apps/lua/lualilka_display.cpp +++ b/firmware/keira/src/apps/lua/lualilka_display.cpp @@ -4,6 +4,7 @@ Arduino_GFX* getDrawable(lua_State* L) { lua_getfield(L, LUA_REGISTRYINDEX, "app"); const App* app = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); return app->canvas; } diff --git a/firmware/keira/src/apps/lua/luarunner.cpp b/firmware/keira/src/apps/lua/luarunner.cpp index adf17093..30ac5279 100644 --- a/firmware/keira/src/apps/lua/luarunner.cpp +++ b/firmware/keira/src/apps/lua/luarunner.cpp @@ -13,6 +13,7 @@ #include "lualilka_geometry.h" #include "lualilka_gpio.h" #include "lualilka_util.h" +#include "lualilka_buzzer.h" #include "lualilka_state.h" jmp_buf stopjmp; @@ -30,7 +31,7 @@ bool pushLilka(lua_State* L) { bool callInit(lua_State* L) { // Calls the "init" function of the "lilka" table. - // Returns false if the function doesn't exist and pops "lilka" from the stack. + // Returns false if the function doesn't exist. Keeps "lilka" on the stack. lua_getfield(L, -1, "init"); if (!lua_isfunction(L, -1)) { lua_pop(L, 1); @@ -53,7 +54,7 @@ bool callUpdate(lua_State* L, uint32_t delta) { // Returns false if the function doesn't exist and pops "lilka" from the stack. lua_getfield(L, -1, "update"); if (!lua_isfunction(L, -1)) { - lua_pop(L, 1); + lua_pop(L, 2); return false; } lua_pushnumber(L, ((float)delta) / 1000.0); @@ -75,7 +76,7 @@ bool callDraw(lua_State* L) { // Returns false if the function doesn't exist and pops "lilka" from the stack. lua_getfield(L, -1, "draw"); if (!lua_isfunction(L, -1)) { - lua_pop(L, 1); + lua_pop(L, 2); return false; } int retCode = lua_pcall(L, 0, 0, 0); @@ -133,6 +134,7 @@ void AbstractLuaRunnerApp::luaSetup(const char* dir) { lualilka_geometry_register(L); lualilka_gpio_register(L); lualilka_util_register(L); + lualilka_buzzer_register(L); // lilka::serial_log("lua: init canvas"); // lilka::Canvas* canvas = new lilka::Canvas(); @@ -224,9 +226,12 @@ int AbstractLuaRunnerApp::execute() { lua_pop(L, 1); queueDraw(); + lua_gc(L, LUA_GCCOLLECT, 0); // TODO: Use LUA_GCSTEP? + // display.renderCanvas(canvas); - // Calculate time spent in update + // Calculate time spent in update & gargage collection + // TODO: Split time spent in update, time spent in draw, time spent in GC? uint32_t elapsed = millis() - now; // If we're too fast, delay to keep 30 FPS if (elapsed < perfectDelta) { @@ -467,6 +472,7 @@ void LuaReplApp::run() { } int retCode = luaL_loadstring(L, input.c_str()) || execute(); + lua_gc(L, LUA_GCCOLLECT, 0); // TODO: Use LUA_GCSTEP? if (retCode) { const char* err = lua_tostring(L, -1); diff --git a/sdk/lib/lilka/src/lilka/buzzer.cpp b/sdk/lib/lilka/src/lilka/buzzer.cpp index cdaff2fd..96286431 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.cpp +++ b/sdk/lib/lilka/src/lilka/buzzer.cpp @@ -20,6 +20,7 @@ void Buzzer::begin() { serial_err("Buzzer is not supported on this board"); return; #else + _stop(); pinMode(LILKA_BUZZER, OUTPUT); #endif } @@ -55,17 +56,24 @@ void Buzzer::playMelody(const Tone* melody, uint32_t length, uint32_t tempo) { xSemaphoreTake(buzzerMutex, portMAX_DELAY); _stop(); currentMelody = static_cast(realloc(currentMelody, length * sizeof(Tone))); + // delete[] currentMelody; + // currentMelody = new Tone[length]; memcpy(currentMelody, melody, length * sizeof(Tone)); currentMelodyLength = length; currentMelodyTempo = tempo; - xTaskCreate(melodyTask, "melodyTask", 2048, this, 1, &melodyTaskHandle); + // Serial.println("Melody task starting, length: " + String(length) + ", tempo: " + String(tempo) + ", first note frequency: " + String(melody[0].frequency)); + if (xTaskCreate(melodyTask, "melodyTask", 2048, this, 1, &melodyTaskHandle) != pdPASS) { + serial_err("Failed to create melody task (not enough memory?)"); + } xSemaphoreGive(buzzerMutex); #endif } void Buzzer::melodyTask(void* arg) { Buzzer* buzzer = static_cast(arg); + // Serial.println("Melody task started"); for (uint32_t i = 0; i < buzzer->currentMelodyLength; i++) { + // Serial.println("Playing note " + String(i) + " with frequency " + String(buzzer->currentMelody[i].frequency)); xSemaphoreTake(buzzer->buzzerMutex, portMAX_DELAY); Tone currentTone = buzzer->currentMelody[i]; if (currentTone.size == 0) { @@ -82,11 +90,9 @@ void Buzzer::melodyTask(void* arg) { } xSemaphoreGive(buzzer->buzzerMutex); vTaskDelay(duration / portTICK_PERIOD_MS); - if (i == buzzer->currentMelodyLength - 1) { - noTone(LILKA_BUZZER); - } } - buzzer->stop(); + noTone(LILKA_BUZZER); + vTaskSuspend(NULL); } void Buzzer::stop() { @@ -100,11 +106,15 @@ void Buzzer::stop() { } void Buzzer::_stop() { - if (melodyTaskHandle != NULL) { - vTaskDelete(melodyTaskHandle); + // TODO: This is not thread-safe + noTone(LILKA_BUZZER); + TaskHandle_t handle = melodyTaskHandle; + if (handle != NULL) { melodyTaskHandle = NULL; + // This can be called from within the task, + // so we postpone the deletion of the task till the end of the function to avoid a deadlock + vTaskDelete(handle); } - noTone(LILKA_BUZZER); } void Buzzer::playDoom() { diff --git a/sdk/lib/lilka/src/lilka/display.cpp b/sdk/lib/lilka/src/lilka/display.cpp index 33c0bfac..afa57033 100644 --- a/sdk/lib/lilka/src/lilka/display.cpp +++ b/sdk/lib/lilka/src/lilka/display.cpp @@ -160,7 +160,8 @@ int16_t Canvas::y() { Image::Image(uint32_t width, uint32_t height, int32_t transparentColor) : width(width), height(height), transparentColor(transparentColor) { - pixels = new uint16_t[width * height]; + // Allocate pixels in PSRAM + pixels = static_cast(ps_malloc(width * height * sizeof(uint16_t))); } Image::~Image() { From 83ec907a91d32911fa4f39d2a3182cee27551636 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 18:40:56 +0200 Subject: [PATCH 05/10] sdk: add buzzer stub files doc: add Lua buzzer docs --- docs/manual/keira/lua/reference/buzzer.rst | 8 ++++ docs/manual/keira/lua/reference/index.rst | 1 + docs/manual/keira/lua/reference/lilka.rst | 1 + sdk/addons/lualilka/library/buzzer.lua | 45 ++++++++++++++++++++++ sdk/addons/lualilka/library/util.lua | 2 +- sdk/lib/lilka/src/lilka/buzzer.cpp | 1 - 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 docs/manual/keira/lua/reference/buzzer.rst create mode 100644 sdk/addons/lualilka/library/buzzer.lua diff --git a/docs/manual/keira/lua/reference/buzzer.rst b/docs/manual/keira/lua/reference/buzzer.rst new file mode 100644 index 00000000..66e913cf --- /dev/null +++ b/docs/manual/keira/lua/reference/buzzer.rst @@ -0,0 +1,8 @@ +``buzzer`` - П'єзо-динамік +-------------------------- + +Функції для роботи з п'єзо-динаміком. + +.. note:: Ці функції не блокують виконання програми: всі звуки та мелодії відтворюються в фоновому режимі. + +.. lua:autoclass:: buzzer diff --git a/docs/manual/keira/lua/reference/index.rst b/docs/manual/keira/lua/reference/index.rst index 0e3b7572..d0000a20 100644 --- a/docs/manual/keira/lua/reference/index.rst +++ b/docs/manual/keira/lua/reference/index.rst @@ -14,4 +14,5 @@ Lua API geometry gpio util + buzzer state diff --git a/docs/manual/keira/lua/reference/lilka.rst b/docs/manual/keira/lua/reference/lilka.rst index d5ef3aa7..500922aa 100644 --- a/docs/manual/keira/lua/reference/lilka.rst +++ b/docs/manual/keira/lua/reference/lilka.rst @@ -6,6 +6,7 @@ Детальніше про їх поведінку можна прочитати в :ref:`секції про написання ігор на Lua `. .. lua:module:: lilka + :noindex: .. lua:function:: init() diff --git a/sdk/addons/lualilka/library/buzzer.lua b/sdk/addons/lualilka/library/buzzer.lua new file mode 100644 index 00000000..57b33293 --- /dev/null +++ b/sdk/addons/lualilka/library/buzzer.lua @@ -0,0 +1,45 @@ +---@meta + +---@class buzzer +buzzer = {} + +---Відтворює звук заданої частоти. +--- +---Якщо передати другий аргумент, звук буде відтворено впродовж цього часу (в мілісекундах). +--- +---@param frequency number частота тону +---@param size? number тривалість звуку (в мілісекундах) +function buzzer.play(frequency, size) end + +---Відтворює мелодію. +--- +---Мелодія - це масив з пар частота-розмірність. +--- +---Наприклад, ноту з тоном ``523`` Гц (нота "до" п'ятої октави) і тривалістю 1/4 можна представити як ``{523, 4}``. +--- +---Від'ємна тривалість означає ноту з крапкою, наприклад: +--- +---* ``-1`` - ціла нота з крапкою (1 + 1/2) +---* ``-2`` - половина з крапкою (1/2 + 1/4) +---* ``-4`` - чверть з крапкою (1/4 + 1/8) +---...і так далі. +--- +---@param melody table мелодія (масив пар частота-розмірність) +---@param tempo number темп мелодії (кількість ударів на хвилину) +---@usage +--- -- Мелодія "до-ре-мі-фа-соль" п'ятої октави +--- local melody = { +--- {523, 2}, +--- {587, 4}, +--- {659, 2}, +--- {698, 4}, +--- {784, 1}, +--- } +--- buzzer.play_melody(melody, 60) -- Відтворює мелодію з темпом 60 ударів на хвилину +function buzzer.play_melody(melody, tempo) end + +---Зупиняє відтворення всіх звуків. +---@return nil +function buzzer.stop() end + +return buzzer diff --git a/sdk/addons/lualilka/library/util.lua b/sdk/addons/lualilka/library/util.lua index 24845004..bf2f2ded 100644 --- a/sdk/addons/lualilka/library/util.lua +++ b/sdk/addons/lualilka/library/util.lua @@ -6,8 +6,8 @@ util = {} function util.time() end ---Затримує виконання скрипта на вказану кількість секунд. ----@usage ---@param sec number кількість секунд, на яку потрібно затримати виконання програми +---@usage --- display.set_cursor(0, 32) --- display.print("Зачекайте півсекунди...") --- util.sleep(0.5) -- Затримує виконання програми на півсекунди. diff --git a/sdk/lib/lilka/src/lilka/buzzer.cpp b/sdk/lib/lilka/src/lilka/buzzer.cpp index 96286431..76dc32d7 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.cpp +++ b/sdk/lib/lilka/src/lilka/buzzer.cpp @@ -106,7 +106,6 @@ void Buzzer::stop() { } void Buzzer::_stop() { - // TODO: This is not thread-safe noTone(LILKA_BUZZER); TaskHandle_t handle = melodyTaskHandle; if (handle != NULL) { From d6ac7cb889ed20e606c5ccbe07baee9ef950d38b Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 19:23:24 +0200 Subject: [PATCH 06/10] keira: add task creation guards lib: make buzzer melody task cleanup thread-safe --- firmware/keira/src/app.cpp | 41 +++++++++++++++++++++++------- sdk/lib/lilka/src/lilka/buzzer.cpp | 18 ++++++++++++- sdk/lib/lilka/src/lilka/buzzer.h | 2 ++ 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/firmware/keira/src/app.cpp b/firmware/keira/src/app.cpp index 59b522c0..6682f033 100644 --- a/firmware/keira/src/app.cpp +++ b/firmware/keira/src/app.cpp @@ -3,16 +3,22 @@ App::App(const char* name) : App(name, 0, 24, lilka::display.width(), lilka::display.height() - 24) { } -App::App(const char* name, uint16_t x, uint16_t y, uint16_t w, uint16_t h) : name(name), x(x), y(y), w(w), h(h) { - canvas = new lilka::Canvas(x, y, w, h); +App::App(const char* name, uint16_t x, uint16_t y, uint16_t w, uint16_t h) : + name(name), + x(x), + y(y), + w(w), + h(h), + flags(AppFlags::APP_FLAG_NONE), + taskHandle(NULL), + canvas(new lilka::Canvas(x, y, w, h)), + backCanvas(new lilka::Canvas(x, y, w, h)), + isDrawQueued(false), + backCanvasMutex(xSemaphoreCreateMutex()) { canvas->begin(); canvas->fillScreen(0); - backCanvas = new lilka::Canvas(x, y, w, h); backCanvas->begin(); backCanvas->fillScreen(0); - backCanvasMutex = xSemaphoreCreateMutex(); - isDrawQueued = false; - flags = AppFlags::APP_FLAG_NONE; Serial.println( "Created app " + String(name) + " at " + String(x) + ", " + String(y) + " with size " + String(w) + "x" + String(h) @@ -20,8 +26,14 @@ App::App(const char* name, uint16_t x, uint16_t y, uint16_t w, uint16_t h) : nam } void App::start() { + if (taskHandle != NULL) { + Serial.println("App " + String(name) + " is already running"); + return; + } Serial.println("Starting app " + String(name)); - xTaskCreate(_run, name, 8192, this, 1, &taskHandle); + if (xTaskCreate(_run, name, 8192, this, 1, &taskHandle) != pdPASS) { + Serial.println("Failed to create task for app " + String(name) + " (not enough memory?)"); + } } void App::_run(void* data) { @@ -34,23 +46,34 @@ void App::_run(void* data) { } void App::suspend() { - // TODO: Check if the task is already suspended + if (taskHandle == NULL) { + Serial.println("App " + String(name) + " is not running, cannot suspend"); + return; + } Serial.println("Suspending app " + String(name) + " (state = " + String(getState()) + ")"); onSuspend(); vTaskSuspend(taskHandle); } void App::resume() { - // TODO: Check if the task is already running + if (taskHandle == NULL) { + Serial.println("App " + String(name) + " is not running, cannot resume"); + return; + } Serial.println("Resuming app " + String(name) + " (state = " + String(getState()) + ")"); onResume(); vTaskResume(taskHandle); } void App::stop() { + if (taskHandle == NULL) { + Serial.println("App " + String(name) + " is not running, cannot stop"); + return; + } Serial.println("Stopping app " + String(name) + " (state = " + String(getState()) + ")"); onStop(); vTaskDelete(taskHandle); + taskHandle = NULL; } App::~App() { diff --git a/sdk/lib/lilka/src/lilka/buzzer.cpp b/sdk/lib/lilka/src/lilka/buzzer.cpp index 76dc32d7..1e2d9ab4 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.cpp +++ b/sdk/lib/lilka/src/lilka/buzzer.cpp @@ -91,8 +91,24 @@ void Buzzer::melodyTask(void* arg) { xSemaphoreGive(buzzer->buzzerMutex); vTaskDelay(duration / portTICK_PERIOD_MS); } + // Start another lambda task to delete the melody task & release mutex + xSemaphoreTake(buzzer->buzzerMutex, portMAX_DELAY); noTone(LILKA_BUZZER); - vTaskSuspend(NULL); + if (xTaskCreate(buzzer->melodyTaskCleanup, "melodyTaskCleanup", 2048, buzzer, 1, NULL) != pdPASS) { + serial_err("Failed to create melody task cleanup task (not enough memory?)"); + // Failsafe: release the mutex & delete task. + // This is NOT thread-safe, but it's better than a deadlock. + xSemaphoreGive(buzzer->buzzerMutex); + vTaskDelete(NULL); + noTone(LILKA_BUZZER); + } +} + +void Buzzer::melodyTaskCleanup(void* arg) { + Buzzer* buzzer = static_cast(arg); + vTaskDelete(buzzer->melodyTaskHandle); + buzzer->melodyTaskHandle = NULL; + xSemaphoreGive(buzzer->buzzerMutex); } void Buzzer::stop() { diff --git a/sdk/lib/lilka/src/lilka/buzzer.h b/sdk/lib/lilka/src/lilka/buzzer.h index 8f6bdd26..042e30ec 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.h +++ b/sdk/lib/lilka/src/lilka/buzzer.h @@ -91,6 +91,8 @@ class Buzzer { private: // cppcheck-suppress unusedPrivateFunction void _stop(); + // cppcheck-suppress unusedPrivateFunction + static void melodyTaskCleanup(void* arg); SemaphoreHandle_t buzzerMutex; TaskHandle_t melodyTaskHandle; From d3eb5ee78a2cb2513b8158bf49cc251b4c93f0c0 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 19:34:21 +0200 Subject: [PATCH 07/10] keira: remove redundant canvas fills --- firmware/keira/src/app.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/firmware/keira/src/app.cpp b/firmware/keira/src/app.cpp index 6682f033..2ade18fa 100644 --- a/firmware/keira/src/app.cpp +++ b/firmware/keira/src/app.cpp @@ -15,10 +15,6 @@ App::App(const char* name, uint16_t x, uint16_t y, uint16_t w, uint16_t h) : backCanvas(new lilka::Canvas(x, y, w, h)), isDrawQueued(false), backCanvasMutex(xSemaphoreCreateMutex()) { - canvas->begin(); - canvas->fillScreen(0); - backCanvas->begin(); - backCanvas->fillScreen(0); Serial.println( "Created app " + String(name) + " at " + String(x) + ", " + String(y) + " with size " + String(w) + "x" + String(h) From 40e7eac83015f5d443020d02a89b0608ee367774 Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 19:49:40 +0200 Subject: [PATCH 08/10] keira: add buzzer notes to lua --- .../keira/src/apps/lua/lualilka_buzzer.cpp | 32 +++++++++++++++++++ sdk/addons/lualilka/library/buzzer.lua | 14 ++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/firmware/keira/src/apps/lua/lualilka_buzzer.cpp b/firmware/keira/src/apps/lua/lualilka_buzzer.cpp index 11c7062b..88ff1473 100644 --- a/firmware/keira/src/apps/lua/lualilka_buzzer.cpp +++ b/firmware/keira/src/apps/lua/lualilka_buzzer.cpp @@ -59,8 +59,40 @@ static const luaL_Reg lualilka_buzzer[] = { }; int lualilka_buzzer_register(lua_State* L) { + using namespace lilka; // Create global "buzzer" table that contains all buzzer functions luaL_newlib(L, lualilka_buzzer); lua_setglobal(L, "buzzer"); + // Create notes table for frequencies from B0 to D#8 + // clang-format off + const lilka::Note notes[] = { + NOTE_B0, + NOTE_C1, NOTE_CS1, NOTE_D1, NOTE_DS1, NOTE_E1, NOTE_F1, NOTE_FS1, NOTE_G1, NOTE_GS1, NOTE_A1, NOTE_AS1, NOTE_B1, + NOTE_C2, NOTE_CS2, NOTE_D2, NOTE_DS2, NOTE_E2, NOTE_F2, NOTE_FS2, NOTE_G2, NOTE_GS2, NOTE_A2, NOTE_AS2, NOTE_B2, + NOTE_C3, NOTE_CS3, NOTE_D3, NOTE_DS3, NOTE_E3, NOTE_F3, NOTE_FS3, NOTE_G3, NOTE_GS3, NOTE_A3, NOTE_AS3, NOTE_B3, + NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4, + NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5, + NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6, + NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7, + NOTE_C8, NOTE_CS8, NOTE_D8, NOTE_DS8, REST, + }; + const char* noteNames[] = { + "B0", + "C1", "CS1", "D1", "DS1", "E1", "F1", "FS1", "G1", "GS1", "A1", "AS1", "B1", + "C2", "CS2", "D2", "DS2", "E2", "F2", "FS2", "G2", "GS2", "A2", "AS2", "B2", + "C3", "CS3", "D3", "DS3", "E3", "F3", "FS3", "G3", "GS3", "A3", "AS3", "B3", + "C4", "CS4", "D4", "DS4", "E4", "F4", "FS4", "G4", "GS4", "A4", "AS4", "B4", + "C5", "CS5", "D5", "DS5", "E5", "F5", "FS5", "G5", "GS5", "A5", "AS5", "B5", + "C6", "CS6", "D6", "DS6", "E6", "F6", "FS6", "G6", "GS6", "A6", "AS6", "B6", + "C7", "CS7", "D7", "DS7", "E7", "F7", "FS7", "G7", "GS7", "A7", "AS7", "B7", + "C8", "CS8", "D8", "DS8", "REST", + }; + // clang-format on + lua_newtable(L); + for (int i = 0; i < sizeof(notes) / sizeof(notes[0]); i++) { + lua_pushinteger(L, notes[i]); + lua_setfield(L, -2, noteNames[i]); + } + lua_setglobal(L, "notes"); return 0; } diff --git a/sdk/addons/lualilka/library/buzzer.lua b/sdk/addons/lualilka/library/buzzer.lua index 57b33293..13b27e7d 100644 --- a/sdk/addons/lualilka/library/buzzer.lua +++ b/sdk/addons/lualilka/library/buzzer.lua @@ -15,7 +15,7 @@ function buzzer.play(frequency, size) end --- ---Мелодія - це масив з пар частота-розмірність. --- ----Наприклад, ноту з тоном ``523`` Гц (нота "до" п'ятої октави) і тривалістю 1/4 можна представити як ``{523, 4}``. +---Наприклад, ноту з частотою ``523`` Гц (нота "до" п'ятої октави) і тривалістю 1/4 можна представити як ``{523, 4}``. --- ---Від'ємна тривалість означає ноту з крапкою, наприклад: --- @@ -24,6 +24,8 @@ function buzzer.play(frequency, size) end ---* ``-4`` - чверть з крапкою (1/4 + 1/8) ---...і так далі. --- +---Якщо частота дорівнює 0, то вона інтерпретується як пауза. +--- ---@param melody table мелодія (масив пар частота-розмірність) ---@param tempo number темп мелодії (кількість ударів на хвилину) ---@usage @@ -33,9 +35,17 @@ function buzzer.play(frequency, size) end --- {587, 4}, --- {659, 2}, --- {698, 4}, ---- {784, 1}, +--- {784, 2}, --- } --- buzzer.play_melody(melody, 60) -- Відтворює мелодію з темпом 60 ударів на хвилину +--- -- Також можна використовувати глобальний модуль notes, де є константи для нот від B0 до D#8: +--- local melody2 = { +--- {notes.C4, 4}, -- "до" четвертої октави +--- {notes.FS4, 2}, -- "фа-дієз" четвертої октави +--- {notes.D5, 1}, -- "ре" п'ятої октави +--- {0, 2}, -- Пауза +--- } +--- buzzer.play_melody(melody2, 60) function buzzer.play_melody(melody, tempo) end ---Зупиняє відтворення всіх звуків. From 8c3fe35ab81fe5b90b25db9c053c3920d182c31b Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 22:27:49 +0200 Subject: [PATCH 09/10] keira: add smart memory allocator for Lua that uses PSRAM if internal RAM usage is too high --- firmware/keira/src/apps/lua/luarunner.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/firmware/keira/src/apps/lua/luarunner.cpp b/firmware/keira/src/apps/lua/luarunner.cpp index 30ac5279..fc2f6cd5 100644 --- a/firmware/keira/src/apps/lua/luarunner.cpp +++ b/firmware/keira/src/apps/lua/luarunner.cpp @@ -96,10 +96,30 @@ AbstractLuaRunnerApp::AbstractLuaRunnerApp(const char* appName) : setFlags(AppFlags::APP_FLAG_FULLSCREEN); } +void* lua_smart_alloc(void* ud, void* ptr, size_t osize, size_t nsize) { + // If there will be less than 32 KB of free RAM after reallocating, use PSRAM allocator. + (void)ud; + (void)osize; + int32_t free_mem = heap_caps_get_free_size(MALLOC_CAP_8BIT); + if (nsize) { + uint32_t caps = MALLOC_CAP_8BIT; + if (free_mem - nsize < 32 * 1024) { + // Less than 32 KB of free RAM after reallocating. Use PSRAM allocator. + caps |= MALLOC_CAP_SPIRAM; + } else { + // More than 32 KB of free RAM after reallocating. Use regular allocator. + } + return heap_caps_realloc(ptr, nsize, caps); + } else { + free(ptr); + return NULL; + } +} + void AbstractLuaRunnerApp::luaSetup(const char* dir) { lilka::serial_log("lua: script dir: %s", dir); - L = luaL_newstate(); + L = lua_newstate(lua_smart_alloc, NULL); lilka::serial_log("lua: init libs"); luaL_openlibs(L); From b39a2f2572a945db521f1ea60489a3471c7ff57f Mon Sep 17 00:00:00 2001 From: Andrew Dunai Date: Mon, 11 Mar 2024 23:53:42 +0200 Subject: [PATCH 10/10] lib: fix buzzer melody task destruction --- sdk/lib/lilka/src/lilka/buzzer.cpp | 16 ++-------------- sdk/lib/lilka/src/lilka/buzzer.h | 2 -- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/sdk/lib/lilka/src/lilka/buzzer.cpp b/sdk/lib/lilka/src/lilka/buzzer.cpp index 1e2d9ab4..cc9c4fa9 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.cpp +++ b/sdk/lib/lilka/src/lilka/buzzer.cpp @@ -91,24 +91,12 @@ void Buzzer::melodyTask(void* arg) { xSemaphoreGive(buzzer->buzzerMutex); vTaskDelay(duration / portTICK_PERIOD_MS); } - // Start another lambda task to delete the melody task & release mutex xSemaphoreTake(buzzer->buzzerMutex, portMAX_DELAY); noTone(LILKA_BUZZER); - if (xTaskCreate(buzzer->melodyTaskCleanup, "melodyTaskCleanup", 2048, buzzer, 1, NULL) != pdPASS) { - serial_err("Failed to create melody task cleanup task (not enough memory?)"); - // Failsafe: release the mutex & delete task. - // This is NOT thread-safe, but it's better than a deadlock. - xSemaphoreGive(buzzer->buzzerMutex); - vTaskDelete(NULL); - noTone(LILKA_BUZZER); - } -} - -void Buzzer::melodyTaskCleanup(void* arg) { - Buzzer* buzzer = static_cast(arg); - vTaskDelete(buzzer->melodyTaskHandle); + // Release the mutex & delete task. buzzer->melodyTaskHandle = NULL; xSemaphoreGive(buzzer->buzzerMutex); + vTaskDelete(NULL); } void Buzzer::stop() { diff --git a/sdk/lib/lilka/src/lilka/buzzer.h b/sdk/lib/lilka/src/lilka/buzzer.h index 042e30ec..8f6bdd26 100644 --- a/sdk/lib/lilka/src/lilka/buzzer.h +++ b/sdk/lib/lilka/src/lilka/buzzer.h @@ -91,8 +91,6 @@ class Buzzer { private: // cppcheck-suppress unusedPrivateFunction void _stop(); - // cppcheck-suppress unusedPrivateFunction - static void melodyTaskCleanup(void* arg); SemaphoreHandle_t buzzerMutex; TaskHandle_t melodyTaskHandle;