pw_thread_embos: add thread creation for embOS Adds thread creation support to embOS. Also updates existing documentation to reflect that this is now supported. Change-Id: Id95dbb5f7b283733b7cd071831ddcf95c0c549d0 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/44144 Reviewed-by: Keir Mierle <keir@google.com> Commit-Queue: Ewout van Bekkum <ewout@google.com> Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
diff --git a/docs/module_structure.rst b/docs/module_structure.rst index 17d3a07..dc2e4b0 100644 --- a/docs/module_structure.rst +++ b/docs/module_structure.rst
@@ -188,6 +188,8 @@ BUILD.gn README.md +.. _module-structure-compile-time-configuration: + Compile-time configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~ Pigweed modules are intended to be used in a wide variety of environments.
diff --git a/docs/os_abstraction_layers.rst b/docs/os_abstraction_layers.rst index 40238ab..8a0d8d8 100644 --- a/docs/os_abstraction_layers.rst +++ b/docs/os_abstraction_layers.rst
@@ -27,7 +27,7 @@ * - `Azure RTOS (formerly ThreadX) <https://azure.microsoft.com/en-us/services/rtos/>`_ - **✔ Supported** * - `SEGGER embOS <https://www.segger.com/products/rtos/embos/>`_ - - *In Progress* + - **✔ Supported** * - Baremetal - *In Progress* * - `Zephyr <https://www.zephyrproject.org/>`_ @@ -313,17 +313,17 @@ - **Thread Creation** - **Thread Id/Sleep/Yield** * - FreeRTOS - - :ref:`module-pw_sync_freertos` - - :ref:`module-pw_sync_freertos` + - :ref:`module-pw_thread_freertos` + - :ref:`module-pw_thread_freertos` * - ThreadX - - :ref:`module-pw_sync_threadx` - - :ref:`module-pw_sync_threadx` + - :ref:`module-pw_thread_threadx` + - :ref:`module-pw_thread_threadx` * - embOS - - Under Development - - :ref:`module-pw_sync_embos` + - :ref:`module-pw_thread_embos` + - :ref:`module-pw_thread_embos` * - STL - - :ref:`module-pw_sync_stl` - - :ref:`module-pw_sync_stl` + - :ref:`module-pw_thread_stl` + - :ref:`module-pw_thread_stl` * - Zephyr - Planned - Planned @@ -475,22 +475,21 @@ That being said, the following concrete areas are being worked on and can be expected to land at some point in the future: -1. Thread creation support for embOS is in progress. -2. We'd like to offer a system clock based timer abstraction facade which can be +1. We'd like to offer a system clock based timer abstraction facade which can be used on either an RTOS or a hardware timer. -3. We are evaluating a less-portable but very useful portability facade for +2. We are evaluating a less-portable but very useful portability facade for event flags / groups. This would make it even easier to ensure all firmware can be fully executed on the host. -4. Cooperative cancellation thread joining along with a ``std::jhtread`` like +3. Cooperative cancellation thread joining along with a ``std::jhtread`` like wrapper is in progress. -5. We'd like to add support for queues, message queues, and similar channel +4. We'd like to add support for queues, message queues, and similar channel abstractions which also support interprocessor communication in a transparent manner. -6. We're interested in supporting asynchronous worker queues and worker queue +5. We're interested in supporting asynchronous worker queues and worker queue pools. -7. Migrate HAL and similar APIs to use deadlines for the backend virtual +6. Migrate HAL and similar APIs to use deadlines for the backend virtual interfaces to permit a smaller vtable which supports both public timeout and deadline semantics. -8. Baremetal support is partially in place today, but it's not ready for use. -9. Most of our APIs today are focused around synchronous blocking APIs, however +7. Baremetal support is partially in place today, but it's not ready for use. +8. Most of our APIs today are focused around synchronous blocking APIs, however we would love to extend this to include asynchronous APIs.
diff --git a/pw_thread/docs.rst b/pw_thread/docs.rst index 99003cd..9b03369 100644 --- a/pw_thread/docs.rst +++ b/pw_thread/docs.rst
@@ -58,7 +58,7 @@ * - ThreadX - :ref:`module-pw_thread_threadx` * - embOS - - Planned + - :ref:`module-pw_thread_embos` * - STL - :ref:`module-pw_thread_stl` * - Zephyr
diff --git a/pw_thread_embos/BUILD b/pw_thread_embos/BUILD index 3205087..d0a23e2 100644 --- a/pw_thread_embos/BUILD +++ b/pw_thread_embos/BUILD
@@ -76,6 +76,59 @@ # currently do not have Bazel support. ) +# This target provides the embOS specific headers needs for thread creation. +pw_cc_library( + name = "thread_headers", + hdrs = [ + "public/pw_thread_embos/config.h", + "public/pw_thread_embos/context.h", + "public/pw_thread_embos/options.h", + "public/pw_thread_embos/thread_inline.h", + "public/pw_thread_embos/thread_native.h", + "public_overrides/pw_thread_backend/thread_inline.h", + "public_overrides/pw_thread_backend/thread_native.h", + ], + includes = [ + "public", + "public_overrides", + ], + deps = [ + ":id", + "//pw_assert", + "//pw_string", + "//pw_thread:thread_headers", + ], + # TODO(pwbug/317): This should depend on embOS but our third parties + # currently do not have Bazel support. +) + +pw_cc_library( + name = "thread", + srcs = [ + "thread.cc", + ], + deps = [ + ":id", + ":thread_headers", + "//pw_assert", + ], + # TODO(pwbug/317): This should depend on embOS but our third parties + # currently do not have Bazel support. +) + +pw_cc_library( + name = "test_threads", + srcs = [ + "test_threads.cc", + ], + deps = [ + "//pw_chrono:system_clock", + "//pw_thread:sleep", + "//pw_thread:test_threads_header", + "//pw_thread:thread_facade", + ], +) + pw_cc_library( name = "yield_headers", hdrs = [
diff --git a/pw_thread_embos/BUILD.gn b/pw_thread_embos/BUILD.gn index 4e76548..383b6a6 100644 --- a/pw_thread_embos/BUILD.gn +++ b/pw_thread_embos/BUILD.gn
@@ -14,10 +14,19 @@ import("//build_overrides/pigweed.gni") +import("$dir_pw_build/module_config.gni") import("$dir_pw_build/target_types.gni") import("$dir_pw_chrono/backend.gni") import("$dir_pw_docgen/docs.gni") import("$dir_pw_thread/backend.gni") +import("$dir_pw_unit_test/test.gni") + +declare_args() { + # The build target that overrides the default configuration options for this + # module. This should point to a source set that provides defines through a + # public config (which may -include a file or add defines directly). + pw_thread_embos_CONFIG = pw_build_DEFAULT_MODULE_CONFIG +} config("public_include_path") { include_dirs = [ "public" ] @@ -29,6 +38,15 @@ visibility = [ ":*" ] } +pw_source_set("config") { + public = [ "public/pw_thread_embos/config.h" ] + public_configs = [ ":public_include_path" ] + public_deps = [ + "$dir_pw_third_party/embos", + pw_thread_embos_CONFIG, + ] +} + # This target provides the backend for pw::thread::Id. pw_source_set("id") { public_configs = [ @@ -77,6 +95,33 @@ } } +# This target provides the backend for pw::thread::Thread and the headers needed +# for thread creation. +pw_source_set("thread") { + public_configs = [ + ":public_include_path", + ":backend_config", + ] + public_deps = [ + ":config", + "$dir_pw_assert", + "$dir_pw_string", + "$dir_pw_third_party/embos", + "$dir_pw_thread:id", + "$dir_pw_thread:thread.facade", + ] + public = [ + "public/pw_thread_embos/context.h", + "public/pw_thread_embos/options.h", + "public/pw_thread_embos/thread_inline.h", + "public/pw_thread_embos/thread_native.h", + "public_overrides/pw_thread_backend/thread_inline.h", + "public_overrides/pw_thread_backend/thread_native.h", + ] + allow_circular_includes_from = [ "$dir_pw_thread:thread.facade" ] + sources = [ "thread.cc" ] +} + # This target provides the backend for pw::thread::yield. pw_source_set("yield") { public_configs = [ @@ -95,6 +140,30 @@ deps = [ "$dir_pw_thread:yield.facade" ] } +pw_test_group("tests") { + tests = [ ":thread_backend_test" ] +} + +pw_source_set("test_threads") { + public_deps = [ "$dir_pw_thread:test_threads" ] + sources = [ "test_threads.cc" ] + deps = [ + "$dir_pw_chrono:system_clock", + "$dir_pw_thread:sleep", + "$dir_pw_thread:thread", + dir_pw_assert, + dir_pw_log, + ] +} + +pw_test("thread_backend_test") { + enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_embos:thread" + deps = [ + ":test_threads", + "$dir_pw_thread:thread_facade_test", + ] +} + pw_doc_group("docs") { sources = [ "docs.rst" ] }
diff --git a/pw_thread_embos/docs.rst b/pw_thread_embos/docs.rst index cad1908..16de090 100644 --- a/pw_thread_embos/docs.rst +++ b/pw_thread_embos/docs.rst
@@ -1,8 +1,150 @@ .. _module-pw_thread_embos: ---------------- +=============== pw_thread_embos ---------------- -This is a set of backends for pw_thread based on embOS v4. It is not ready for -use, and is under construction. +=============== +This is a set of backends for pw_thread based on embOS v4. +.. contents:: + :local: + :depth: 1 + +.. Warning:: + This module is still under construction, the API is not yet stable. + +----------------------- +Thread Creation Backend +----------------------- +A backend or ``pw::thread::Thread`` is offered using ``OS_CreateTaskEx()``. +Optional joining support is enabled via an ``OS_EVENT`` in each thread's +context. + +This backend permits users to start threads where contexts must be explicitly +allocated and passed in as an option. As a quick example, a detached thread +can be created as follows: + +.. code-block:: cpp + + #include "pw_thread/detached_thread.h" + #include "pw_thread_embos/context.h" + #include "pw_thread_embos/options.h" + #include "RTOS.h" // For the embOS types. + + pw::thread::embos::ContextWithStack<42> example_thread_context; + void StartExampleThread() { + pw::thread::DetachedThread( + pw::thread::embos::Options() + .set_name("static_example_thread") + .set_priority(kFooPriority) + .set_time_slice_interval(kFooTimeSliceInterval) + .set_context(example_thread_context), + example_thread_function) + } + + +Module Configuration Options +============================ +The following configurations can be adjusted via compile-time configuration of +this module, see the +:ref:`module documentation <module-structure-compile-time-configuration>` for +more details. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_JOINING_ENABLED + + Whether thread joining is enabled. By default this is disabled. + + We suggest only enabling this when thread joining is required to minimize + the RAM and ROM cost of threads. + + Enabling this grows the RAM footprint of every pw::thread::Thread as it adds + an OS_EVENT to every thread's pw::thread::embos::Context. In addition, there + is a minute ROM cost to construct and destroy this added object. + + PW_THREAD_JOINING_ENABLED gets set to this value. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_MINIMUM_STACK_SIZE_WORDS + + The minimum stack size in words. By default this uses Segger's recommendation + of 68 bytes. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_DEFAULT_STACK_SIZE_WORDS + + The default stack size in words. By default this uses Segger's recommendation + of 256 bytes to start. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN + + The maximum length of a thread's name, not including null termination. By + default this is arbitrarily set to 15. This results in an array of characters + which is this length + 1 bytes in every ``pw::thread::Thread``'s context. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY + + The minimum priority level, this is normally 1, since 0 is not a valid + priority level. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_DEFAULT_PRIORITY + + The default priority level. By default this uses the minimal embOS priority. + +.. c:macro:: PW_THREAD_EMBOS_CONFIG_DEFAULT_TIME_SLICE_INTERVAL + + The round robin time slice tick interval for threads at the same priority. + By default this is set to 2 ticks based on the embOS default. + + +embOS Thread Options +==================== +.. cpp:class:: pw::thread::embos::Options + + .. cpp:function:: set_name(const char* name) + + Sets the name for the embOS task, this is optional. + Note that this will be deep copied into the context and may be truncated + based on ``PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN``. + + .. cpp:function:: set_priority(OS_PRIO priority) + + Sets the priority for the embOS task. Higher values are higher priority, + see embOS OS_CreateTaskEx for more detail. + Precondition: This must be >= ``PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY``. + + .. cpp:function:: set_time_slice_interval(OS_UINT time_slice_interval) + + Sets the number of ticks this thread is allowed to run before other ready + threads of the same priority are given a chance to run. + + A value of 0 disables time-slicing of this thread. + + Precondition: This must be <= 255 ticks. + + .. cpp:function:: set_context(pw::thread::embos::Context& context) + + Set the pre-allocated context (all memory needed to run a thread). Note that + this is required for this thread creation backend! The ``Context`` can + either be constructed with an externally provided ``std::span<OS_UINT>`` + stack or the templated form of ``ContextWithStack<kStackSizeWords>`` can + be used. + + +----------------------------- +Thread Identification Backend +----------------------------- +A backend for ``pw::thread::Id`` and ``pw::thread::get_id()`` is offerred using +``OS_GetTaskID()``. It uses ``DASSERT`` to ensure that the scheduler has started +via ``OS_IsRunning()``. + +-------------------- +Thread Sleep Backend +-------------------- +A backend for ``pw::thread::sleep_for()`` and ``pw::thread::sleep_until()`` is +offerred using ``OS_Delay()`` if the duration is at least one tick, else +``OS_Yield()`` is used. It uses ``pw::this_thread::get_id() != thread::Id()`` to +ensure it invoked only from a thread. + +-------------------- +Thread Yield Backend +-------------------- +A backend for ``pw::thread::yield()`` is offered using via ``OS_Yield()``. +It uses ``pw::this_thread::get_id() != thread::Id()`` to ensure it invoked only +from a thread.
diff --git a/pw_thread_embos/public/pw_thread_embos/config.h b/pw_thread_embos/public/pw_thread_embos/config.h new file mode 100644 index 0000000..f277f27 --- /dev/null +++ b/pw_thread_embos/public/pw_thread_embos/config.h
@@ -0,0 +1,85 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// Configuration macros for the tokenizer module. +#pragma once + +#include "RTOS.h" + +// Whether thread joining is enabled. By default this is disabled. +// +// We suggest only enabling this when thread joining is required to minimize +// the RAM and ROM cost of threads. +// +// Enabling this grows the RAM footprint of every pw::thread::Thread as it adds +// an OS_EVENT to every thread's pw::thread::embos::Context. In addition, there +// is a minute ROM cost to construct and destroy this added object. +// +// PW_THREAD_JOINING_ENABLED gets set to this value. +#ifndef PW_THREAD_EMBOS_CONFIG_JOINING_ENABLED +#define PW_THREAD_EMBOS_CONFIG_JOINING_ENABLED 0 +#endif // PW_THREAD_EMBOS_CONFIG_JOINING_ENABLED +#define PW_THREAD_JOINING_ENABLED PW_THREAD_EMBOS_CONFIG_JOINING_ENABLED + +// The minimum stack size in words. By default this uses Segger's recommendation +// of 68 bytes. +#ifndef PW_THREAD_EMBOS_CONFIG_MINIMUM_STACK_SIZE_WORDS +#define PW_THREAD_EMBOS_CONFIG_MINIMUM_STACK_SIZE_WORDS (68 / sizeof(OS_UINT)) +#endif // PW_THREAD_EMBOS_CONFIG_MINIMUM_STACK_SIZE_WORDS + +// The default stack size in words. By default this uses Segger's recommendation +// of 256 bytes to start. +#ifndef PW_THREAD_EMBOS_CONFIG_DEFAULT_STACK_SIZE_WORDS +#define PW_THREAD_EMBOS_CONFIG_DEFAULT_STACK_SIZE_WORDS (256 / sizeof(OS_UINT)) +#endif // PW_THREAD_EMBOS_CONFIG_DEFAULT_STACK_SIZE_WORDS + +// The maximum length of a thread's name, not including null termination. By +// default this is arbitrarily set to 15. This results in an array of characters +// which is this length + 1 bytes in every pw::thread::Thread's context. +#ifndef PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN +#define PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN 15 +#endif // PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN + +// The minimum priority level, this is normally 1, since 0 is not a valid +// priority level. +#ifndef PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY +#define PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY 1 +#endif // PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY + +// The default priority level. By default this uses the minimal embOS priority. +#ifndef PW_THREAD_EMBOS_CONFIG_DEFAULT_PRIORITY +#define PW_THREAD_EMBOS_CONFIG_DEFAULT_PRIORITY \ + PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY +#endif // PW_THREAD_EMBOS_CONFIG_DEFAULT_PRIORITY + +// The round robin time slice tick interval for threads at the same priority. +// By default this is set to 2 ticks based on the embOS default. +#ifndef PW_THREAD_EMBOS_CONFIG_DEFAULT_TIME_SLICE_INTERVAL +#define PW_THREAD_EMBOS_CONFIG_DEFAULT_TIME_SLICE_INTERVAL 2 +#endif // PW_THREAD_EMBOS_CONFIG_DEFAULT_TIME_SLICE_INTERVAL + +namespace pw::thread::embos::config { + +inline constexpr size_t kMaximumNameLength = + PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN; +inline constexpr size_t kMinimumStackSizeWords = + PW_THREAD_EMBOS_CONFIG_MINIMUM_STACK_SIZE_WORDS; +inline constexpr size_t kDefaultStackSizeWords = + PW_THREAD_EMBOS_CONFIG_DEFAULT_STACK_SIZE_WORDS; +inline constexpr OS_PRIO kMinimumPriority = PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY; +inline constexpr OS_PRIO kDefaultPriority = + PW_THREAD_EMBOS_CONFIG_DEFAULT_PRIORITY; +inline constexpr OS_PRIO kDefaultTimeSliceInterval = + PW_THREAD_EMBOS_CONFIG_DEFAULT_TIME_SLICE_INTERVAL; + +} // namespace pw::thread::embos::config
diff --git a/pw_thread_embos/public/pw_thread_embos/context.h b/pw_thread_embos/public/pw_thread_embos/context.h new file mode 100644 index 0000000..ffce52f --- /dev/null +++ b/pw_thread_embos/public/pw_thread_embos/context.h
@@ -0,0 +1,129 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include <cstring> +#include <span> + +#include "RTOS.h" +#include "pw_string/util.h" +#include "pw_thread_embos/config.h" + +namespace pw::thread { + +class Thread; // Forward declare Thread which depends on Context. + +} // namespace pw::thread + +namespace pw::thread::embos { + +// Static thread context allocation including the TCB, an event group for +// joining if enabled, and an external statically allocated stack. +// +// Example usage: +// +// std::array<ULONG, 42> example_thread_stack; +// pw::thread::embos::Context example_thread_context(example_thread_stack); +// void StartExampleThread() { +// pw::thread::DetachedThread( +// pw::thread::embos::Options() +// .set_name("static_example_thread") +// .set_priority(kFooPriority) +// .set_context(example_thread_context), +// example_thread_function); +// } +class Context { + public: + explicit Context(std::span<OS_UINT> stack_span) + : tcb_{}, stack_span_(stack_span) {} + Context(const Context&) = delete; + Context& operator=(const Context&) = delete; + + // Intended for unit test & Thread use only. + OS_TASK& tcb() { return tcb_; } + + private: + friend Thread; + + std::span<OS_UINT> stack() { return stack_span_; } + + bool in_use() const { return in_use_; } + void set_in_use(bool in_use = true) { in_use_ = in_use; } + + const char* name() const { return name_.data(); } + void set_name(const char* name) { string::Copy(name, name_); } + + using ThreadRoutine = void (*)(void* arg); + void set_thread_routine(ThreadRoutine entry, void* arg) { + user_thread_entry_function_ = entry; + user_thread_entry_arg_ = arg; + } + + bool detached() const { return detached_; } + void set_detached(bool value = true) { detached_ = value; } + + bool thread_done() const { return thread_done_; } + void set_thread_done(bool value = true) { thread_done_ = value; } + +#if PW_THREAD_JOINING_ENABLED + OS_EVENT& join_event_object() { return event_object_; } +#endif // PW_THREAD_JOINING_ENABLED + + static void ThreadEntryPoint(void* void_context_ptr); + static void TerminateThread(Context& context); + + OS_TASK tcb_; + std::span<OS_UINT> stack_span_; + + ThreadRoutine user_thread_entry_function_ = nullptr; + void* user_thread_entry_arg_ = nullptr; +#if PW_THREAD_JOINING_ENABLED + // Note that the embOS life cycle of this event object is managed together + // with the thread life cycle, not this object's life cycle. + OS_EVENT event_object_; +#endif // PW_THREAD_JOINING_ENABLED + bool in_use_ = false; + bool detached_ = false; + bool thread_done_ = false; + + // The TCB does not have storage for the name, ergo we provide storage for + // the thread's name which can be truncated down to just a null delimiter. + std::array<char, config::kMaximumNameLength + 1> name_; +}; + +// Static thread context allocation including the stack along with the Context. +// +// Example usage: +// +// pw::thread::embos::ContextWithStack<42> example_thread_context; +// void StartExampleThread() { +// pw::thread::DetachedThread( +// pw::thread::embos::Options() +// .set_name("static_example_thread") +// .set_priority(kFooPriority) +// .set_context(example_thread_context), +// example_thread_function); +// } +template <size_t kStackSizeWords = config::kDefaultStackSizeWords> +class ContextWithStack final : public Context { + public: + constexpr ContextWithStack() : Context(stack_storage_) { + static_assert(kStackSizeWords >= config::kMinimumStackSizeWords); + } + + private: + std::array<OS_UINT, kStackSizeWords> stack_storage_; +}; + +} // namespace pw::thread::embos
diff --git a/pw_thread_embos/public/pw_thread_embos/options.h b/pw_thread_embos/public/pw_thread_embos/options.h new file mode 100644 index 0000000..fb33066 --- /dev/null +++ b/pw_thread_embos/public/pw_thread_embos/options.h
@@ -0,0 +1,100 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include "RTOS.h" +#include "pw_assert/assert.h" +#include "pw_thread/thread.h" +#include "pw_thread_embos/config.h" +#include "pw_thread_embos/context.h" + +namespace pw::thread::embos { + +// pw::thread::Options for FreeRTOS. +// +// Example usage: +// +// // Uses the default priority, but specifies a custom name and context. +// pw::thread::Thread example_thread( +// pw::thread::embos::Options() +// .set_name("example_thread"), +// .set_context(static_example_thread_context), +// example_thread_function); +// +// // Provides the name, priority, and pre-allocated context. +// pw::thread::Thread static_example_thread( +// pw::thread::embos::Options() +// .set_name("static_example_thread") +// .set_priority(kFooPriority) +// .set_context(static_example_thread_context), +// example_thread_function); +// +class Options : public thread::Options { + public: + constexpr Options() = default; + constexpr Options(const Options&) = default; + constexpr Options(Options&& other) = default; + + // Sets the name for the embOS task, this is optional. + // Note that this will be deep copied into the context and may be truncated + // based on PW_THREAD_EMBOS_CONFIG_MAX_THREAD_NAME_LEN. + constexpr Options set_name(const char* name) { + name_ = name; + return *this; + } + + // Sets the priority for the embOS task. Higher values are higher priority, + // see embOS OS_CreateTaskEx for more detail. + // + // Precondition: This must be >= PW_THREAD_EMBOS_CONFIG_MIN_PRIORITY. + constexpr Options set_priority(OS_PRIO priority) { + PW_DASSERT(priority >= config::kMinimumPriority); + priority_ = priority; + return *this; + } + + // Sets the number of ticks this thread is allowed to run before other ready + // threads of the same priority are given a chance to run. + // + // A value of 0 disables time-slicing of this thread. + // + // Precondition: This must be <= 255 ticks. + constexpr Options set_time_slice_interval(OS_UINT time_slice_interval) { + PW_DASSERT(time_slice_interval <= 255); + time_slice_interval_ = time_slice_interval; + return *this; + } + + // Set the pre-allocated context (all memory needed to run a thread), see the + // pw::thread::embos::Context for more detail. + constexpr Options set_context(Context& context) { + context_ = &context; + return *this; + } + + private: + friend thread::Thread; + + const char* name() const { return name_; } + OS_PRIO priority() const { return priority_; } + OS_PRIO time_slice_interval() const { return time_slice_interval_; } + Context* context() const { return context_; } + + const char* name_ = nullptr; + OS_PRIO priority_ = config::kDefaultPriority; + OS_PRIO time_slice_interval_ = config::kDefaultTimeSliceInterval; + Context* context_ = nullptr; +}; + +} // namespace pw::thread::embos
diff --git a/pw_thread_embos/public/pw_thread_embos/thread_inline.h b/pw_thread_embos/public/pw_thread_embos/thread_inline.h new file mode 100644 index 0000000..ed8cc4f --- /dev/null +++ b/pw_thread_embos/public/pw_thread_embos/thread_inline.h
@@ -0,0 +1,49 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include <algorithm> + +#include "pw_assert/assert.h" +#include "pw_thread/id.h" +#include "pw_thread_embos/context.h" + +namespace pw::thread { + +inline Thread::Thread() : native_type_(nullptr) {} + +inline Thread& Thread::operator=(Thread&& other) { + native_type_ = other.native_type_; + other.native_type_ = nullptr; + return *this; +} + +inline Thread::~Thread() { PW_DASSERT(native_type_ == nullptr); } + +inline Id Thread::get_id() const { + if (native_type_ == nullptr) { + return Id(nullptr); + } + return Id(&native_type_->tcb()); +} + +inline void Thread::swap(Thread& other) { + std::swap(native_type_, other.native_type_); +} + +inline Thread::native_handle_type Thread::native_handle() { + return native_type_; +} + +} // namespace pw::thread
diff --git a/pw_thread_embos/public/pw_thread_embos/thread_native.h b/pw_thread_embos/public/pw_thread_embos/thread_native.h new file mode 100644 index 0000000..ba7a339 --- /dev/null +++ b/pw_thread_embos/public/pw_thread_embos/thread_native.h
@@ -0,0 +1,26 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include "pw_thread_embos/context.h" + +namespace pw::thread::backend { + +// The native thread is a pointer to a thread's context. +using NativeThread = pw::thread::embos::Context*; + +// The native thread handle is the same as the NativeThread. +using NativeThreadHandle = pw::thread::embos::Context*; + +} // namespace pw::thread::backend
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_embos/public_overrides/pw_thread_backend/thread_inline.h new file mode 100644 index 0000000..fd5df5c --- /dev/null +++ b/pw_thread_embos/public_overrides/pw_thread_backend/thread_inline.h
@@ -0,0 +1,16 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include "pw_thread_embos/thread_inline.h"
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/thread_native.h b/pw_thread_embos/public_overrides/pw_thread_backend/thread_native.h new file mode 100644 index 0000000..f8dd122 --- /dev/null +++ b/pw_thread_embos/public_overrides/pw_thread_backend/thread_native.h
@@ -0,0 +1,16 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include "pw_thread_embos/thread_native.h"
diff --git a/pw_thread_embos/test_threads.cc b/pw_thread_embos/test_threads.cc new file mode 100644 index 0000000..db95071 --- /dev/null +++ b/pw_thread_embos/test_threads.cc
@@ -0,0 +1,62 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_thread/test_threads.h" + +#include <chrono> + +#include "RTOS.h" +#include "pw_assert/check.h" +#include "pw_chrono/system_clock.h" +#include "pw_log/log.h" +#include "pw_thread/sleep.h" +#include "pw_thread_embos/context.h" +#include "pw_thread_embos/options.h" + +namespace pw::thread::test { +namespace { + +std::array<embos::ContextWithStack<>, 2> thread_contexts; + +} // namespace + +const Options& TestOptionsThread0() { + static constexpr embos::Options thread_0_options = + embos::Options() + .set_name("pw::TestThread0") + .set_context(thread_contexts[0]); + return thread_0_options; +} + +const Options& TestOptionsThread1() { + static constexpr embos::Options thread_1_options = + embos::Options() + .set_name("pw::TestThread1") + .set_context(thread_contexts[1]); + return thread_1_options; +} + +void WaitUntilDetachedThreadsCleanedUp() { + // embOS does not permit us to invoke a callback after the TCB has been + // unregistered from the kernel, however it does provide an API to query this + // status. + for (auto& context : thread_contexts) { + while (OS_IsTask(&context.tcb())) { + this_thread::sleep_for( + chrono::SystemClock::for_at_least(std::chrono::milliseconds(1))); + } + } +} + +} // namespace pw::thread::test
diff --git a/pw_thread_embos/thread.cc b/pw_thread_embos/thread.cc new file mode 100644 index 0000000..d8f95d5 --- /dev/null +++ b/pw_thread_embos/thread.cc
@@ -0,0 +1,160 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#include "pw_thread/thread.h" + +#include "pw_assert/check.h" +#include "pw_thread/id.h" +#include "pw_thread_embos/config.h" +#include "pw_thread_embos/context.h" +#include "pw_thread_embos/options.h" + +using pw::thread::embos::Context; + +namespace pw::thread { + +void Context::ThreadEntryPoint(void* void_context_ptr) { + Context& context = *reinterpret_cast<Context*>(void_context_ptr); + + // Invoke the user's thread function. This may never return. + context.user_thread_entry_function_(context.user_thread_entry_arg_); + + // Use a task only critical section to guard against join() and detach(). + OS_SuspendAllTasks(); + if (context.detached()) { + // There is no threadsafe way to re-use detached threads. Callbacks + // registered through OS_AddOnTerminateHook CANNOT be used for this as they + // are invoked before the kernel is done using the task's TCB! + // However to enable unit test coverage we go ahead and clear this. + context.set_in_use(false); + +#if PW_THREAD_JOINING_ENABLED + // If the thread handle was detached before the thread finished execution, + // i.e. got here, then we are responsible for cleaning up the join event + // object. + OS_EVENT_Delete(&context.join_event_object()); +#endif // PW_THREAD_JOINING_ENABLED + + // Re-enable the scheduler before we delete this execution. Note this name + // is a bit misleading as reference counting is used. + OS_ResumeAllSuspendedTasks(); + OS_TerminateTask(nullptr); + PW_UNREACHABLE; + } + + // Otherwise the task finished before the thread was detached or joined, defer + // cleanup to Thread's join() or detach(). + context.set_thread_done(); + OS_ResumeAllSuspendedTasks(); + +#if PW_THREAD_JOINING_ENABLED + OS_EVENT_Set(&context.join_event_object()); +#endif // PW_THREAD_JOINING_ENABLED + + // Let the thread handle owner terminate this task when they detach or join. + OS_Suspend(nullptr); + PW_UNREACHABLE; +} + +void Context::TerminateThread(Context& context) { + // Stop the other task first. + OS_TerminateTask(&context.tcb()); + + // Mark the context as unused for potential later re-use. + context.set_in_use(false); + +#if PW_THREAD_JOINING_ENABLED + // Just in case someone abused our API, ensure their use of the event group is + // properly handled by the kernel regardless. + OS_EVENT_Delete(&context.join_event_object()); +#endif // PW_THREAD_JOINING_ENABLED +} + +Thread::Thread(const thread::Options& facade_options, + ThreadRoutine entry, + void* arg) + : native_type_(nullptr) { + // Cast the generic facade options to the backend specific option of which + // only one type can exist at compile time. + auto options = static_cast<const embos::Options&>(facade_options); + PW_DCHECK_NOTNULL(options.context(), "The Context is not optional"); + native_type_ = options.context(); + + // Can't use a context more than once. + PW_DCHECK(!native_type_->in_use()); + + // Reset the state of the static context in case it was re-used. + native_type_->set_in_use(true); + native_type_->set_detached(false); + native_type_->set_thread_done(false); +#if PW_THREAD_JOINING_ENABLED + OS_EVENT_CreateEx(&options.context()->join_event_object(), + OS_EVENT_RESET_MODE_AUTO); +#endif // PW_THREAD_JOINING_ENABLED + + // Copy over the thread name. + native_type_->set_name(options.name()); + + // In order to support functions which return and joining, a delegate is + // deep copied into the context with a small wrapping function to actually + // invoke the task with its arg. + native_type_->set_thread_routine(entry, arg); + + OS_CreateTaskEx(&options.context()->tcb(), + native_type_->name(), + options.priority(), + Context::ThreadEntryPoint, + options.context()->stack().data(), + static_cast<OS_UINT>(options.context()->stack().size_bytes()), + options.time_slice_interval(), + options.context()); +} + +void Thread::detach() { + PW_CHECK(joinable()); + + OS_Suspend(&native_type_->tcb()); + native_type_->set_detached(); + const bool thread_done = native_type_->thread_done(); + OS_Resume(&native_type_->tcb()); + + if (thread_done) { + // The task finished (hit end of Context::ThreadEntryPoint) before we + // invoked detach, clean up the thread. + Context::TerminateThread(*native_type_); + } else { + // We're detaching before the task finished, defer cleanup to the task at + // the end of Context::ThreadEntryPoint. + } + + // Update to no longer represent a thread of execution. + native_type_ = nullptr; +} + +#if PW_THREAD_JOINING_ENABLED +void Thread::join() { + PW_CHECK(joinable()); + PW_CHECK(this_thread::get_id() != get_id()); + + OS_EVENT_Wait(&native_type_->join_event_object()); + + // No need for a critical section here as the thread at this point is + // waiting to be deleted. + Context::TerminateThread(*native_type_); + + // Update to no longer represent a thread of execution. + native_type_ = nullptr; +} +#endif // PW_THREAD_JOINING_ENABLED + +} // namespace pw::thread