From 22d142cfaf42669bef33c86610436ae1d236ec1f Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 27 Jan 2026 21:42:18 +0100 Subject: [PATCH] Implemented C thread and added test --- CODING_STYLE_C.md | 38 ++- .../Include/tactility/concurrent/thread.h | 182 ++++++++++++ TactilityKernel/Source/concurrent/thread.cpp | 258 ++++++++++++++++++ Tests/TactilityKernel/ThreadTest.cpp | 112 ++++++++ 4 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 TactilityKernel/Include/tactility/concurrent/thread.h create mode 100644 TactilityKernel/Source/concurrent/thread.cpp create mode 100644 Tests/TactilityKernel/ThreadTest.cpp diff --git a/CODING_STYLE_C.md b/CODING_STYLE_C.md index f48219db..3ef70188 100644 --- a/CODING_STYLE_C.md +++ b/CODING_STYLE_C.md @@ -1,6 +1,6 @@ # C coding Style -## Naming +## Files & Folders ### Files @@ -8,7 +8,7 @@ Files are lower snake case. - Files: `^[0-9a-z_]+$` - Directories: `^[0-9a-z_]+$` - + Example: ```c some_feature.c @@ -22,6 +22,8 @@ Project folders include: - `private` for private header files - `include` for projects that require separate header files +## C language + ### Macros and consts These are all upper snake case: @@ -94,3 +96,35 @@ Examples: ```c typedef uint32_t thread_id_t; ``` + +### Function comments + +```c +/** + * @brief Validates a number + * @param[in] number the integer to validate + * @return true if validation was succesful and there were no issues + */ +bool validate(int number); + +/** + * @brief Run the action. + * @param timeout[in] the maximum time the task should run + * @retval ERROR_TIMEOUT when the task couldn't be completed on time + * @retval ERROR_NONE when the task completed successfully + */ +error_t runAction(TickType_t timeout); + +/** + * @brief Increase a number. + * @param[inout] number + */ +void increase(int* number); + +/** + * A function with a longer description here. + * + * @brief short description + */ +void something(); +``` diff --git a/TactilityKernel/Include/tactility/concurrent/thread.h b/TactilityKernel/Include/tactility/concurrent/thread.h new file mode 100644 index 00000000..b78a5714 --- /dev/null +++ b/TactilityKernel/Include/tactility/concurrent/thread.h @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "tactility/error.h" +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef ESP_PLATFORM +#include +#endif + +#include +#include + +#include +#include +#include + +typedef enum { + THREAD_STATE_STOPPED, + THREAD_STATE_STARTING, + THREAD_STATE_RUNNING, +} ThreadState; + +/** ThreadPriority */ +enum ThreadPriority { + THREAD_PRIORITY_NONE = 0U, + THREAD_PRIORITY_IDLE = 1U, + THREAD_PRIORITY_LOWER = 2U, + THREAD_PRIORITY_LOW = 3U, + THREAD_PRIORITY_NORMAL = 4U, + THREAD_PRIORITY_HIGH = 5U, + THREAD_PRIORITY_HIGHER = 6U, + THREAD_PRIORITY_CRITICAL = 7U +}; + +typedef int32_t (*thread_main_fn_t)(void* context); +typedef void (*thread_state_callback_t)(ThreadState state, void* context); + +struct Thread; +typedef struct Thread Thread; + +/** + * @brief Creates a new thread instance with default settings. + * @return A pointer to the created Thread instance, or NULL if allocation failed. + */ +Thread* thread_alloc(void); + +/** + * @brief Creates a new thread instance with specified parameters. + * @param[in] name The name of the thread. + * @param[in] stack_size The size of the thread stack in bytes. + * @param[in] function The main function to be executed by the thread. + * @param[in] function_context A pointer to the context to be passed to the main function. + * @param[in] affinity The CPU core affinity for the thread (e.g., tskNO_AFFINITY). + * @return A pointer to the created Thread instance, or NULL if allocation failed. + */ +Thread* thread_alloc_full( + const char* name, + configSTACK_DEPTH_TYPE stack_size, + thread_main_fn_t function, + void* function_context, + portBASE_TYPE affinity +); + +/** + * @brief Destroys a thread instance. + * @param[in] thread The thread instance to destroy. + * @note The thread must be in the STOPPED state. + */ +void thread_free(Thread* thread); + +/** + * @brief Sets the name of the thread. + * @param[in] thread The thread instance. + * @param[in] name The new name for the thread. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_name(Thread* thread, const char* name); + +/** + * @brief Sets the stack size for the thread. + * @param[in] thread The thread instance. + * @param[in] stack_size The stack size in bytes. Must be a multiple of 4. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_stack_size(Thread* thread, size_t stack_size); + +/** + * @brief Sets the CPU core affinity for the thread. + * @param[in] thread The thread instance. + * @param[in] affinity The CPU core affinity. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_affinity(Thread* thread, portBASE_TYPE affinity); + +/** + * @brief Sets the main function and context for the thread. + * @param[in] thread The thread instance. + * @param[in] function The main function to be executed. + * @param[in] context A pointer to the context to be passed to the main function. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_main_function(Thread* thread, thread_main_fn_t function, void* context); + +/** + * @brief Sets the priority for the thread. + * @param[in] thread The thread instance. + * @param[in] priority The thread priority. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_priority(Thread* thread, enum ThreadPriority priority); + +/** + * @brief Sets a callback to be invoked when the thread state changes. + * @param[in] thread The thread instance. + * @param[in] callback The callback function. + * @param[in] context A pointer to the context to be passed to the callback function. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_state_callback(Thread* thread, thread_state_callback_t callback, void* context); + +/** + * @brief Gets the current state of the thread. + * @param[in] thread The thread instance. + * @return The current ThreadState. + */ +ThreadState thread_get_state(Thread* thread); + +/** + * @brief Starts the thread execution. + * @param[in] thread The thread instance. + * @note The thread must be in the STOPPED state and have a main function set. + * @retval ERROR_NONE when the thread was started + * @retval ERROR_UNDEFINED when the thread failed to start + */ +error_t thread_start(Thread* thread); + +/** + * @brief Waits for the thread to finish execution. + * @param[in] thread The thread instance. + * @param[in] timeout The maximum time to wait in ticks. + * @param[in] poll_interval The interval between status checks in ticks. + * @retval ERROR_NONE when the thread was stopped + * @retval ERROR_TIMEOUT when the thread was not stopped because the timeout has passed + * @note Cannot be called from the thread being joined. + */ +error_t thread_join(Thread* thread, TickType_t timeout, TickType_t poll_interval); + +/** + * @brief Gets the FreeRTOS task handle associated with the thread. + * @param[in] thread The thread instance. + * @return The TaskHandle_t, or NULL if the thread is not running. + */ +TaskHandle_t thread_get_task_handle(Thread* thread); + +/** + * @brief Gets the return code from the thread's main function. + * @param[in] thread The thread instance. + * @return The return code of the thread's main function. + * @note The thread must be in the STOPPED state. + */ +int32_t thread_get_return_code(Thread* thread); + +/** + * @brief Gets the minimum remaining stack space for the thread since it started. + * @param[in] thread The thread instance. + * @return The minimum remaining stack space in bytes. + * @note The thread must be in the RUNNING state. + */ +uint32_t thread_get_stack_space(Thread* thread); + +/** + * @brief Gets the current thread instance. + * @return A pointer to the current Thread instance, or NULL if not called from a thread created by this module. + */ +Thread* thread_get_current(void); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/Source/concurrent/thread.cpp b/TactilityKernel/Source/concurrent/thread.cpp new file mode 100644 index 00000000..020632e7 --- /dev/null +++ b/TactilityKernel/Source/concurrent/thread.cpp @@ -0,0 +1,258 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const size_t LOCAL_STORAGE_SELF_POINTER_INDEX = 0; +static const char* TAG = LOG_TAG("Thread"); + +struct Thread { + TaskHandle_t taskHandle = nullptr; + ThreadState state = THREAD_STATE_STOPPED; + thread_main_fn_t mainFunction = nullptr; + void* mainFunctionContext = nullptr; + int32_t callbackResult = 0; + thread_state_callback_t stateCallback = nullptr; + void* stateCallbackContext = nullptr; + std::string name = "unnamed"; + enum ThreadPriority priority = THREAD_PRIORITY_NORMAL; + struct Mutex mutex = { 0 }; + configSTACK_DEPTH_TYPE stackSize = 4096; + portBASE_TYPE affinity = -1; + + Thread() { + mutex_construct(&mutex); + } + + ~Thread() { + mutex_destruct(&mutex); + } + + void lock() { mutex_lock(&mutex); } + + void unlock() { mutex_unlock(&mutex); } +}; + +static void thread_set_state_internal(Thread* thread, ThreadState newState) { + thread->lock(); + thread->state = newState; + if (thread->stateCallback) { + thread->stateCallback(thread->state, thread->stateCallbackContext); + } + thread->unlock(); +} + +static void thread_main_body(void* context) { + check(context != nullptr); + auto* thread = static_cast(context); + + // Save Thread instance pointer to task local storage + check(pvTaskGetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX) == nullptr); + vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, thread); + + LOG_I(TAG, "Starting %s", thread->name.c_str()); + check(thread->state == THREAD_STATE_STARTING); + thread_set_state_internal(thread, THREAD_STATE_RUNNING); + + thread->callbackResult = thread->mainFunction(thread->mainFunctionContext); + + check(thread->state == THREAD_STATE_RUNNING); + thread_set_state_internal(thread, THREAD_STATE_STOPPED); + LOG_I(TAG, "Stopped %s", thread->name.c_str()); + + vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr); + thread->taskHandle = nullptr; + + vTaskDelete(nullptr); +} + +extern "C" { + +Thread* thread_alloc(void) { + auto* thread = new(std::nothrow) Thread(); + if (thread == nullptr) { + return nullptr; + } + return thread; +} + +Thread* thread_alloc_full( + const char* name, + configSTACK_DEPTH_TYPE stack_size, + thread_main_fn_t function, + void* function_context, + portBASE_TYPE affinity +) { + auto* thread = new(std::nothrow) Thread(); + if (thread != nullptr) { + thread_set_name(thread, name); + thread_set_stack_size(thread, stack_size); + thread_set_main_function(thread, function, function_context); + thread_set_affinity(thread, affinity); + } + return thread; +} + +void thread_free(Thread* thread) { + check(thread); + check(thread->state == THREAD_STATE_STOPPED); + check(thread->taskHandle == nullptr); + delete thread; +} + +void thread_set_name(Thread* thread, const char* name) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->name = name; + thread->unlock(); +} + +void thread_set_stack_size(Thread* thread, size_t stack_size) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + check(stack_size % 4 == 0); + thread->stackSize = stack_size; + thread->unlock(); +} + +void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->affinity = affinity; + thread->unlock(); +} + +void thread_set_main_function(Thread* thread, thread_main_fn_t function, void* context) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->mainFunction = function; + thread->mainFunctionContext = context; + thread->unlock(); +} + +void thread_set_priority(Thread* thread, enum ThreadPriority priority) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->priority = priority; + thread->unlock(); +} + +void thread_set_state_callback(Thread* thread, thread_state_callback_t callback, void* context) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->stateCallback = callback; + thread->stateCallbackContext = context; + thread->unlock(); +} + +ThreadState thread_get_state(Thread* thread) { + check(xPortInIsrContext() == pdFALSE); + thread->lock(); + ThreadState state = thread->state; + thread->unlock(); + return state; +} + +error_t thread_start(Thread* thread) { + thread->lock(); + check(thread->mainFunction != nullptr); + check(thread->state == THREAD_STATE_STOPPED); + check(thread->stackSize); + thread->unlock(); + + thread_set_state_internal(thread, THREAD_STATE_STARTING); + + thread->lock(); + uint32_t stack_depth = thread->stackSize / sizeof(StackType_t); + enum ThreadPriority priority = thread->priority; + portBASE_TYPE affinity = thread->affinity; + thread->unlock(); + + BaseType_t result; + if (affinity != -1) { +#ifdef ESP_PLATFORM + result = xTaskCreatePinnedToCore( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle, + affinity + ); +#else + result = xTaskCreate( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle + ); +#endif + } else { + result = xTaskCreate( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle + ); + } + + return (result == pdPASS) ? ERROR_NONE : ERROR_UNDEFINED; +} + +error_t thread_join(Thread* thread, TickType_t timeout, TickType_t poll_interval) { + check(thread_get_current() != thread); + + TickType_t start_ticks = get_ticks(); + while (thread_get_task_handle(thread)) { + delay_ticks(poll_interval); + if (get_ticks() - start_ticks > timeout) { + return ERROR_TIMEOUT; + } + } + + return ERROR_NONE; +} + +TaskHandle_t thread_get_task_handle(Thread* thread) { + thread->lock(); + auto* handle = thread->taskHandle; + thread->unlock(); + return handle; +} + +int32_t thread_get_return_code(Thread* thread) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + auto result = thread->callbackResult; + thread->unlock(); + return result; +} + +uint32_t thread_get_stack_space(Thread* thread) { + if (xPortInIsrContext() == pdTRUE) { + return 0; + } + thread->lock(); + assert(thread->state == THREAD_STATE_RUNNING); + auto result = uxTaskGetStackHighWaterMark(thread->taskHandle) * sizeof(StackType_t); + thread->unlock(); + return result; +} + +Thread* thread_get_current(void) { + check(xPortInIsrContext() == pdFALSE); + return (Thread*)pvTaskGetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX); +} + +} diff --git a/Tests/TactilityKernel/ThreadTest.cpp b/Tests/TactilityKernel/ThreadTest.cpp new file mode 100644 index 00000000..a89b0c13 --- /dev/null +++ b/Tests/TactilityKernel/ThreadTest.cpp @@ -0,0 +1,112 @@ +#include "doctest.h" +#include "tactility/delay.h" + +#include + +TEST_CASE("when a thread is started then its callback should be called") { + bool has_called = false; + auto* thread = thread_alloc_full( + "immediate return task", + 4096, + [](void* context) { + auto* has_called_ptr = static_cast(context); + *has_called_ptr = true; + return 0; + }, + &has_called, + -1 + ); + + CHECK(!has_called); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); + CHECK(has_called); +} + +TEST_CASE("a thread can be started and stopped") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + ); + + CHECK(thread); + CHECK_EQ(thread_start(thread), ERROR_NONE); + interrupted = true; + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); +} + +TEST_CASE("thread id should only be set at when thread is started") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + ); + CHECK_EQ(thread_get_task_handle(thread), nullptr); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_NE(thread_get_task_handle(thread), nullptr); + interrupted = true; + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + CHECK_EQ(thread_get_task_handle(thread), nullptr); + thread_free(thread); +} + +TEST_CASE("thread state should be correct") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + + ); + CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED); + thread_start(thread); + auto state = thread_get_state(thread); + CHECK((state == THREAD_STATE_STARTING || state == THREAD_STATE_RUNNING)); + interrupted = true; + CHECK_EQ(thread_join(thread, 10, 1), ERROR_NONE); + CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED); + thread_free(thread); +} + +TEST_CASE("thread id should only be set at when thread is started") { + auto* thread = thread_alloc_full( + "return code", + 4096, + [](void* context) { return 123; }, + nullptr, + -1 + ); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(thread_join(thread, 1, 1), ERROR_NONE); + CHECK_EQ(thread_get_return_code(thread), 123); + thread_free(thread); +}