summaryrefslogtreecommitdiff
path: root/src/tasks
diff options
context:
space:
mode:
Diffstat (limited to 'src/tasks')
-rw-r--r--src/tasks/CMakeLists.txt2
-rw-r--r--src/tasks/tasks.cpp205
-rw-r--r--src/tasks/tasks.hpp106
3 files changed, 306 insertions, 7 deletions
diff --git a/src/tasks/CMakeLists.txt b/src/tasks/CMakeLists.txt
index 0503d293..f7d7244f 100644
--- a/src/tasks/CMakeLists.txt
+++ b/src/tasks/CMakeLists.txt
@@ -1,2 +1,2 @@
-idf_component_register(SRCS "tasks.cpp" INCLUDE_DIRS ".")
+idf_component_register(SRCS "tasks.cpp" INCLUDE_DIRS "." REQUIRES "span")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})
diff --git a/src/tasks/tasks.cpp b/src/tasks/tasks.cpp
index b9fce7ec..0d9d7881 100644
--- a/src/tasks/tasks.cpp
+++ b/src/tasks/tasks.cpp
@@ -1,5 +1,204 @@
#include "tasks.hpp"
+#include <functional>
+#include "esp_heap_caps.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/portmacro.h"
-const UBaseType_t kTaskPriorityLvgl = 4;
-const UBaseType_t kTaskPriorityAudioPipeline = 5;
-const UBaseType_t kTaskPriorityAudioDrain = 6;
+namespace tasks {
+
+template <Type t>
+auto Name() -> std::string;
+
+template <>
+auto Name<Type::kUi>() -> std::string {
+ return "LVGL";
+}
+template <>
+auto Name<Type::kUiFlush>() -> std::string {
+ return "DISPLAY";
+}
+template <>
+auto Name<Type::kAudio>() -> std::string {
+ return "AUDIO";
+}
+template <>
+auto Name<Type::kAudioDrain>() -> std::string {
+ return "DRAIN";
+}
+template <>
+auto Name<Type::kDatabase>() -> std::string {
+ return "DB";
+}
+
+template <Type t>
+auto AllocateStack() -> cpp::span<StackType_t>;
+
+// Decoders run on the audio task, and these sometimes require a fairly large
+// amount of stack space.
+template <>
+auto AllocateStack<Type::kAudio>() -> cpp::span<StackType_t> {
+ std::size_t size = 32 * 1024;
+ return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)),
+ size};
+}
+template <>
+auto AllocateStack<Type::kAudioDrain>() -> cpp::span<StackType_t> {
+ std::size_t size = 1024;
+ return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)),
+ size};
+}
+// LVGL requires only a relatively small stack. However, it can be allocated in
+// PSRAM so we give it a bit of headroom for safety.
+template <>
+auto AllocateStack<Type::kUi>() -> cpp::span<StackType_t> {
+ std::size_t size = 16 * 1024;
+ return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)),
+ size};
+}
+// UI flushes *must* be done from internal RAM. Thankfully, there is very little
+// stack required to perform them, and the amount of stack needed is fixed.
+template <>
+auto AllocateStack<Type::kUiFlush>() -> cpp::span<StackType_t> {
+ std::size_t size = 1024;
+ return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)),
+ size};
+}
+// Leveldb is designed for non-embedded use cases, where stack space isn't so
+// much of a concern. It therefore uses an eye-wateringly large amount of stack.
+template <>
+auto AllocateStack<Type::kDatabase>() -> cpp::span<StackType_t> {
+ std::size_t size = 256 * 1024;
+ return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)),
+ size};
+}
+
+// 2048 bytes in internal ram
+// 302 KiB in external ram.
+
+/*
+ * Please keep the priorities below in descending order for better readability.
+ */
+
+template <Type t>
+auto Priority() -> UBaseType_t;
+
+// Realtime audio is the entire point of this device, so give this task the
+// highest priority.
+template <>
+auto Priority<Type::kAudio>() -> UBaseType_t {
+ return 10;
+}
+template <>
+auto Priority<Type::kAudioDrain>() -> UBaseType_t {
+ return 10;
+}
+// After audio issues, UI jank is the most noticeable kind of scheduling-induced
+// slowness that the user is likely to notice or care about. Therefore we place
+// this task directly below audio in terms of priority.
+template <>
+auto Priority<Type::kUi>() -> UBaseType_t {
+ return 9;
+}
+// UI flushing should use the same priority as the UI task, so as to maximise
+// the chance of the happy case: one of our cores is writing to the screen,
+// whilst the other is simultaneously preparing the next buffer to be flushed.
+template <>
+auto Priority<Type::kUiFlush>() -> UBaseType_t {
+ return 9;
+}
+// Database interactions are all inherently async already, due to their
+// potential for disk access. The user likely won't notice or care about a
+// couple of ms extra delay due to scheduling, so give this task the lowest
+// priority.
+template <>
+auto Priority<Type::kDatabase>() -> UBaseType_t {
+ return 8;
+}
+
+template <Type t>
+auto WorkerQueueSize() -> std::size_t;
+
+template <>
+auto WorkerQueueSize<Type::kDatabase>() -> std::size_t {
+ return 8;
+}
+
+template <>
+auto WorkerQueueSize<Type::kUiFlush>() -> std::size_t {
+ return 2;
+}
+
+auto PersistentMain(void* fn) -> void {
+ auto* function = reinterpret_cast<std::function<void(void)>*>(fn);
+ std::invoke(*function);
+ assert("persistent task quit!" == 0);
+ vTaskDelete(NULL);
+}
+
+auto Worker::Main(void* instance) {
+ Worker* i = reinterpret_cast<Worker*>(instance);
+ while (1) {
+ WorkItem item;
+ if (xQueueReceive(i->queue_, &item, portMAX_DELAY)) {
+ if (item.quit) {
+ break;
+ } else if (item.fn != nullptr) {
+ std::invoke(*item.fn);
+ delete item.fn;
+ }
+ }
+ }
+ i->is_task_running_.store(false);
+ i->is_task_running_.notify_all();
+ // Wait for the instance's destructor to delete this task. We do this instead
+ // of just deleting ourselves so that it's 100% certain that it's safe to
+ // delete or reuse this task's stack.
+ while (1) {
+ vTaskDelay(portMAX_DELAY);
+ }
+}
+
+Worker::Worker(const std::string& name,
+ cpp::span<StackType_t> stack,
+ std::size_t queue_size,
+ UBaseType_t priority)
+ : stack_(stack.data()),
+ queue_(xQueueCreate(queue_size, sizeof(WorkItem))),
+ is_task_running_(true),
+ task_buffer_(),
+ task_(xTaskCreateStatic(&Main,
+ name.c_str(),
+ stack.size(),
+ this,
+ priority,
+ stack_,
+ &task_buffer_)) {}
+
+Worker::~Worker() {
+ WorkItem item{
+ .fn = nullptr,
+ .quit = true,
+ };
+ xQueueSend(queue_, &item, portMAX_DELAY);
+ is_task_running_.wait(true);
+ vTaskDelete(task_);
+ free(stack_);
+}
+
+template <>
+auto Worker::Dispatch(const std::function<void(void)>& fn)
+ -> std::future<void> {
+ std::shared_ptr<std::promise<void>> promise =
+ std::make_shared<std::promise<void>>();
+ WorkItem item{
+ .fn = new std::function<void(void)>([=]() {
+ std::invoke(fn);
+ promise->set_value();
+ }),
+ .quit = false,
+ };
+ xQueueSend(queue_, &item, portMAX_DELAY);
+ return promise->get_future();
+}
+
+} // namespace tasks
diff --git a/src/tasks/tasks.hpp b/src/tasks/tasks.hpp
index 47668aea..9f37131e 100644
--- a/src/tasks/tasks.hpp
+++ b/src/tasks/tasks.hpp
@@ -1,7 +1,107 @@
#pragma once
+#include <atomic>
+#include <functional>
+#include <future>
+#include <memory>
+#include <string>
+
+#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
+#include "freertos/projdefs.h"
+#include "freertos/queue.h"
+#include "freertos/task.h"
+#include "span.hpp"
+
+namespace tasks {
+
+/*
+ * Enumeration of every task (basically a thread) started within the firmware.
+ * These are centralised so that it is easier to reason about the relative
+ * priorities of tasks, as well as the amount and location of memory allocated
+ * to each one.
+ */
+enum class Type {
+ // The main UI task. This runs the LVGL main loop.
+ kUi,
+ // Task for flushing graphics buffers to the display.
+ kUiFlush,
+ // The main audio pipeline task.
+ kAudio,
+ // Task for flushing PCM samples to the current output.
+ kAudioDrain,
+ // Task for running database queries.
+ kDatabase,
+};
+
+template <Type t>
+auto Name() -> std::string;
+template <Type t>
+auto AllocateStack() -> cpp::span<StackType_t>;
+template <Type t>
+auto Priority() -> UBaseType_t;
+template <Type t>
+auto WorkerQueueSize() -> std::size_t;
+
+auto PersistentMain(void* fn) -> void;
+
+template <Type t>
+auto StartPersistent(const std::function<void(void)>& fn) -> void {
+ StaticTask_t* task_buffer = new StaticTask_t;
+ cpp::span<StackType_t> stack = AllocateStack<t>();
+ xTaskCreateStatic(&PersistentMain, Name<t>().c_str(), stack.size(),
+ new std::function<void(void)>(fn), Priority<t>(),
+ stack.data(), task_buffer);
+}
+
+class Worker {
+ private:
+ Worker(const std::string& name,
+ cpp::span<StackType_t> stack,
+ std::size_t queue_size,
+ UBaseType_t priority);
+
+ StackType_t* stack_;
+ QueueHandle_t queue_;
+ std::atomic<bool> is_task_running_;
+ StaticTask_t task_buffer_;
+ TaskHandle_t task_;
+
+ struct WorkItem {
+ std::function<void(void)>* fn;
+ bool quit;
+ };
+
+ public:
+ template <Type t>
+ static auto Start() -> Worker* {
+ return new Worker(Name<t>(), AllocateStack<t>(), WorkerQueueSize<t>(),
+ Priority<t>());
+ }
+
+ static auto Main(void* instance);
+
+ /*
+ * Schedules the given function to be executed on the worker task, and
+ * asynchronously returns the result as a future.
+ */
+ template <typename T>
+ auto Dispatch(const std::function<T(void)>& fn) -> std::future<T> {
+ std::shared_ptr<std::promise<T>> promise =
+ std::make_shared<std::promise<T>>();
+ WorkItem item{
+ .fn = new std::function([=]() { promise->set_value(std::invoke(fn)); }),
+ .quit = false,
+ };
+ xQueueSend(queue_, &item, portMAX_DELAY);
+ return promise->get_future();
+ }
+
+ ~Worker();
+};
+
+/* Specialisation of Evaluate for functions that return nothing. */
+template <>
+auto Worker::Dispatch(const std::function<void(void)>& fn) -> std::future<void>;
-extern const UBaseType_t kTaskPriorityLvgl;
-extern const UBaseType_t kTaskPriorityAudioPipeline;
-extern const UBaseType_t kTaskPriorityAudioDrain;
+} // namespace tasks