diff options
author | Ewout van Bekkum <ewout@google.com> | 2021-03-05 15:05:37 -0800 |
---|---|---|
committer | CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2021-04-13 00:00:30 +0000 |
commit | f4da489b23c03963af9c82f631674d76f55065b3 (patch) | |
tree | d429a77b5122037ffdf945afa11bc70dc84d1a35 | |
parent | 5a0450d263b7de5e593ed97d4d902b9719977421 (diff) | |
download | pigweed-f4da489b23c03963af9c82f631674d76f55065b3.tar.gz |
pw_thread_threadx: Enable pw::thread::Thread for ThreadX
Change-Id: Ied43a085786a60d7b1a0681ab51c8d00a0eb0319
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/40381
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
-rw-r--r-- | docs/BUILD.gn | 2 | ||||
-rw-r--r-- | pw_thread_threadx/BUILD | 63 | ||||
-rw-r--r-- | pw_thread_threadx/BUILD.gn | 77 | ||||
-rw-r--r-- | pw_thread_threadx/docs.rst | 27 | ||||
-rw-r--r-- | pw_thread_threadx/public/pw_thread_threadx/config.h | 75 | ||||
-rw-r--r-- | pw_thread_threadx/public/pw_thread_threadx/context.h | 135 | ||||
-rw-r--r-- | pw_thread_threadx/public/pw_thread_threadx/options.h | 141 | ||||
-rw-r--r-- | pw_thread_threadx/public/pw_thread_threadx/thread_inline.h | 50 | ||||
-rw-r--r-- | pw_thread_threadx/public/pw_thread_threadx/thread_native.h | 26 | ||||
-rw-r--r-- | pw_thread_threadx/public_overrides/pw_thread_backend/thread_inline.h | 16 | ||||
-rw-r--r-- | pw_thread_threadx/public_overrides/pw_thread_backend/thread_native.h | 16 | ||||
-rw-r--r-- | pw_thread_threadx/test_threads.cc | 72 | ||||
-rw-r--r-- | pw_thread_threadx/thread.cc | 201 |
13 files changed, 891 insertions, 10 deletions
diff --git a/docs/BUILD.gn b/docs/BUILD.gn index 57ec63704..b35db1e95 100644 --- a/docs/BUILD.gn +++ b/docs/BUILD.gn @@ -113,6 +113,8 @@ group("module_docs") { "$dir_pw_sys_io_baremetal_stm32f429:docs", "$dir_pw_sys_io_stdio:docs", "$dir_pw_target_runner:docs", + "$dir_pw_thread:docs", + "$dir_pw_thread_threadx:docs", "$dir_pw_tokenizer:docs", "$dir_pw_toolchain:docs", "$dir_pw_trace:docs", diff --git a/pw_thread_threadx/BUILD b/pw_thread_threadx/BUILD index bcb8a1f43..427af06d9 100644 --- a/pw_thread_threadx/BUILD +++ b/pw_thread_threadx/BUILD @@ -1,4 +1,4 @@ -# Copyright 2020 The Pigweed Authors +# 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 @@ -15,6 +15,7 @@ load( "//pw_build:pigweed.bzl", "pw_cc_library", + "pw_cc_test", ) package(default_visibility = ["//visibility:public"]) @@ -45,6 +46,66 @@ pw_cc_library( # currently do not have Bazel support. ) +# This target provides the ThreadX specific headers needs for thread creation. +pw_cc_library( + name = "thread_headers", + hdrs = [ + "public/pw_thread_threadx/context.h", + "public/pw_thread_threadx/options.h", + "public/pw_thread_threadx/config.h", + "public/pw_thread_threadx/thread_inline.h", + "public/pw_thread_threadx/thread_native.h", + "public_overrides/pw_thread_backend/thread_inline.h", + "public_overrides/pw_thread_backend/thread_native.h", + ], + includes = [ + "public", + "public_overrides", + ], + deps = [ + "//pw_assert", + ":id", + "//pw_thread:thread_headers", + ], + # TODO(pwbug/317): This should depend on ThreadX but our third parties + # currently do not have Bazel support. +) + +pw_cc_library( + name = "thread", + srcs = [ + "thread.cc", + ], + deps = [ + "//pw_assert", + ":id", + ":thread_headers", + ], + # TODO(pwbug/317): This should depend on ThreadX but our third parties + # currently do not have Bazel support. +) + +pw_cc_library( + name = "test_threads", + deps = [ + "//pw_thread:thread_facade", + "//pw_thread:test_threads_header", + "//pw_chrono:system_clock", + "//pw_thread:sleep", + ], + srcs = [ + "test_threads.cc", + ] +) + +pw_cc_test( + name = "thread_backend_test", + deps = [ + "//pw_thread:thread_facade_test", + ":test_threads", + ] +) + pw_cc_library( name = "sleep_headers", hdrs = [ diff --git a/pw_thread_threadx/BUILD.gn b/pw_thread_threadx/BUILD.gn index 954615f40..f7a090b1b 100644 --- a/pw_thread_threadx/BUILD.gn +++ b/pw_thread_threadx/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright 2020 The Pigweed Authors +# 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 @@ -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_threadx_CONFIG = pw_build_DEFAULT_MODULE_CONFIG +} config("public_include_path") { include_dirs = [ "public" ] @@ -29,6 +38,15 @@ config("backend_config") { visibility = [ ":*" ] } +pw_source_set("config") { + public = [ "public/pw_thread_threadx/config.h" ] + public_configs = [ ":public_include_path" ] + public_deps = [ + "$dir_pw_third_party/threadx", + pw_thread_threadx_CONFIG, + ] +} + # This target provides the backend for pw::thread::Id. pw_source_set("id") { public_configs = [ @@ -50,7 +68,7 @@ pw_source_set("id") { } if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") { - # This target provides the backend for pw::thread::sleep_{for,until}. + # This target provides the backend for pw::this_thread::sleep_{for,until}. pw_source_set("sleep") { public_configs = [ ":public_include_path", @@ -72,12 +90,37 @@ if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") { pw_thread_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK || pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_threadx:system_clock", - "The ThreadX pw::thread::sleep_{for,until} backend only works with " + - "the ThreadX pw::chrono::SystemClock backend.") + "The ThreadX pw::this_thread::sleep_{for,until} backend only works with " + "the ThreadX pw::chrono::SystemClock backend.") } } -# This target provides the backend for pw::thread::yield. +# 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_third_party/threadx", + "$dir_pw_thread:id", + "$dir_pw_thread:thread.facade", + ] + public = [ + "public/pw_thread_threadx/context.h", + "public/pw_thread_threadx/options.h", + "public/pw_thread_threadx/thread_inline.h", + "public/pw_thread_threadx/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::this_thread::yield. pw_source_set("yield") { public_configs = [ ":public_include_path", @@ -95,6 +138,30 @@ pw_source_set("yield") { 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_threadx:thread" + deps = [ + ":test_threads", + "$dir_pw_thread:thread_facade_test", + ] +} + pw_doc_group("docs") { sources = [ "docs.rst" ] } diff --git a/pw_thread_threadx/docs.rst b/pw_thread_threadx/docs.rst index a4579781f..527b487e1 100644 --- a/pw_thread_threadx/docs.rst +++ b/pw_thread_threadx/docs.rst @@ -1,8 +1,27 @@ .. _module-pw_thread_threadx: ------------------ +================= pw_thread_threadx ------------------ -This is a set of backends for pw_thread based on ThreadX. It is not ready for -use, and is under construction. +================= +This is a set of backends for pw_thread based on ThreadX. +.. Warning:: + This module is still under construction, the API is not yet stable. + +.. list-table:: + + * - :ref:`module-pw_thread` Facade + - Backend Target + - Description + * - ``pw_thread:id`` + - ``pw_thread_threadx:id`` + - Thread identification. + * - ``pw_thread:yield`` + - ``pw_thread_threadx:yield`` + - Thread scheduler yielding. + * - ``pw_thread:sleep`` + - ``pw_thread_threadx:sleep`` + - Thread scheduler sleeping. + * - ``pw_thread:thread`` + - ``pw_thread_threadx:thread`` + - Thread creation. diff --git a/pw_thread_threadx/public/pw_thread_threadx/config.h b/pw_thread_threadx/public/pw_thread_threadx/config.h new file mode 100644 index 000000000..0198f4b97 --- /dev/null +++ b/pw_thread_threadx/public/pw_thread_threadx/config.h @@ -0,0 +1,75 @@ +// 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 "tx_api.h" + +// Whether thread joining is enabled. By default this is disabled. +// When enabled this adds a TX_EVENT_FLAGS_GROUP to every pw::thread::Thread's +// context. +#ifndef PW_THREAD_THREADX_CONFIG_JOINING_ENABLED +#define PW_THREAD_THREADX_CONFIG_JOINING_ENABLED 0 +#endif // PW_THREAD_THREADX_CONFIG_JOINING_ENABLED +#define PW_THREAD_JOINING_ENABLED PW_THREAD_THREADX_CONFIG_JOINING_ENABLED + +// The default stack size in words. By default this uses the minimal ThreadX +// stack size. +#ifndef PW_THREAD_THREADX_CONFIG_DEFAULT_STACK_SIZE_WORDS +#define PW_THREAD_THREADX_CONFIG_DEFAULT_STACK_SIZE_WORDS \ + TX_MINIMUM_STACK / sizeof(ULONG) +#endif // PW_THREAD_THREADX_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_THREADX_CONFIG_MAX_THREAD_NAME_LEN +#define PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN 15 +#endif // PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN + +// The round robin time slice tick interval for threads at the same priority. +// By default this is disabled as not all ports support this, using a value of 0 +// ticks. +#ifndef PW_THREAD_THREADX_CONFIG_DEFAULT_TIME_SLICE_INTERVAL +#define PW_THREAD_THREADX_CONFIG_DEFAULT_TIME_SLICE_INTERVAL TX_NO_TIME_SLICE +#endif // PW_THREAD_THREADX_CONFIG_DEFAULT_TIME_SLICE_INTERVAL + +// The minimum priority level, this is normally based on the number of priority +// levels. +#ifndef PW_THREAD_THREADX_CONFIG_MIN_PRIORITY +#define PW_THREAD_THREADX_CONFIG_MIN_PRIORITY TX_MAX_PRIORITIES - 1 +#endif // PW_THREAD_THREADX_CONFIG_MIN_PRIORITY + +// The default stack size in words. By default this uses the minimal ThreadX +// priority level, given that 0 is the highest priority. +#ifndef PW_THREAD_THREADX_CONFIG_DEFAULT_PRIORITY +#define PW_THREAD_THREADX_CONFIG_DEFAULT_PRIORITY \ + PW_THREAD_THREADX_CONFIG_MIN_PRIORITY +#endif // PW_THREAD_THREADX_CONFIG_DEFAULT_PRIORITY + +namespace pw::thread::threadx::config { + +inline constexpr size_t kMaximumNameLength = + PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN + 1; +inline constexpr size_t kMinimumStackSizeWords = + TX_MINIMUM_STACK / sizeof(ULONG); +inline constexpr size_t kDefaultStackSizeWords = + PW_THREAD_THREADX_CONFIG_DEFAULT_STACK_SIZE_WORDS; +inline constexpr UINT kMinimumPriority = PW_THREAD_THREADX_CONFIG_MIN_PRIORITY; +inline constexpr UINT kDefaultPriority = + PW_THREAD_THREADX_CONFIG_DEFAULT_PRIORITY; +inline constexpr ULONG kDefaultTimeSliceInterval = + PW_THREAD_THREADX_CONFIG_DEFAULT_TIME_SLICE_INTERVAL; + +} // namespace pw::thread::threadx::config diff --git a/pw_thread_threadx/public/pw_thread_threadx/context.h b/pw_thread_threadx/public/pw_thread_threadx/context.h new file mode 100644 index 000000000..162e3b306 --- /dev/null +++ b/pw_thread_threadx/public/pw_thread_threadx/context.h @@ -0,0 +1,135 @@ +// 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 <cstdint> +#include <cstring> +#include <span> + +#include "pw_thread_threadx/config.h" +#include "tx_api.h" +#include "tx_thread.h" + +namespace pw::thread { + +class Thread; // Forward declare Thread which depends on Context. + +} // namespace pw::thread + +namespace pw::thread::threadx { + +// 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::threadx::Context example_thread_context(example_thread_stack); +// void StartExampleThread() { +// pw::thread::Thread( +// pw::thread::threadx::Options() +// .set_name("static_example_thread") +// .set_priority(kFooPriority) +// .set_static_context(example_thread_context), +// example_thread_function).detach(); +// } +class Context { + public: + explicit Context(std::span<ULONG> stack_span) + : tcb_{}, stack_span_(stack_span) {} + Context(const Context&) = delete; + Context& operator=(const Context&) = delete; + + // Intended for unit test & Thread use only. + TX_THREAD& tcb() { return tcb_; } + + private: + friend Thread; + + std::span<ULONG> 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) { + strncpy(name_.data(), name, name_.size() - 1); + // Forcefully NULL terminate as strncpy does not NULL terminate if the count + // limit is hit. + name_[name_.size() - 1] = '\0'; + } + + using ThreadRoutine = void (*)(void* arg); + void set_thread_routine(ThreadRoutine entry, void* arg) { + entry_ = 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 + TX_EVENT_FLAGS_GROUP& join_event_group() { return event_group_; } +#endif // PW_THREAD_JOINING_ENABLED + + static void RunThread(ULONG void_context_ptr); + static void DeleteThread(Context& context); + + TX_THREAD tcb_; + std::span<ULONG> stack_span_; + + ThreadRoutine entry_ = nullptr; + void* arg_ = nullptr; +#if PW_THREAD_JOINING_ENABLED + // Note that the ThreadX life cycle of this event group is managed together + // with the thread life cycle, not this object's life cycle. + TX_EVENT_FLAGS_GROUP event_group_; +#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 delimeter. + std::array<char, config::kMaximumNameLength> name_; +}; + +// Static thread context allocation including the stack along with the Context. +// +// Example usage: +// +// pw::thread::threadx::ContextWithStack<42> example_thread_context; +// void StartExampleThread() { +// pw::thread::Thread( +// pw::thread::threadx::Options() +// .set_name("static_example_thread") +// .set_priority(kFooPriority) +// .set_static_context(example_thread_context), +// example_thread_function).detach(); +// } +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<ULONG, kStackSizeWords> stack_storage_; +}; + +} // namespace pw::thread::threadx diff --git a/pw_thread_threadx/public/pw_thread_threadx/options.h b/pw_thread_threadx/public/pw_thread_threadx/options.h new file mode 100644 index 000000000..cdd3414fd --- /dev/null +++ b/pw_thread_threadx/public/pw_thread_threadx/options.h @@ -0,0 +1,141 @@ +// 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_assert/assert.h" +#include "pw_thread/thread.h" +#include "pw_thread_threadx/config.h" +#include "pw_thread_threadx/context.h" +#include "tx_api.h" + +namespace pw::thread::threadx { + +// pw::thread::Options for ThreadX. +// +// Example usage: +// +// // Uses the default priority and time slice interval (which may be +// // disabled), but specifies a custom name and pre-allocated context. +// // Note that the preemption threshold is disabled by default. +// pw::thread::Thread example_thread( +// pw::thread::threadx::Options() +// .set_name("example_thread"), +// .set_context(static_example_thread_context), +// example_thread_function); +// +// // Specifies the name, priority, time slice interval, and pre-allocated +// // context, but does not use a preemption threshold. +// pw::thread::Thread static_example_thread( +// pw::thread::threadx::Options() +// .set_name("static_example_thread") +// .set_priority(kFooPriority) +// .set_time_slice_interval(1) +// .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 ThreadX thread, note that this will be deep copied + // into the context and may be truncated based on + // PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN. + constexpr Options set_name(const char* name) { + name_ = name; + return *this; + } + + // Sets the priority for the ThreadX thread from 0 through 31, where a value + // of 0 represents the highest priority, see ThreadX tx_thread_create for + // more detail. + constexpr Options set_priority(UINT priority) { + PW_DASSERT(priority <= PW_THREAD_THREADX_CONFIG_MIN_PRIORITY); + priority_ = priority; + return *this; + } + + // Optionally sets the preemption threshold for the ThreadX thread from 0 + // through 31. + // + // Only priorities higher than this level (i.e. lower number) are allowed to + // preempt this thread. In other words this allows the thread to specify the + // priority ceiling for disabling preemption. Threads that have a higher + // priority than the ceiling are still allowed to preempt while those with + // less than the ceiling are not allowed to preempt. + // + // Not setting the preemption threshold or explicitly specifying a value + // equal to the priority disables preemption threshold. + // + // Time slicing is disabled while the preemption threshold is enabled, i.e. + // not equal to the priority, even if a time slice interval was specified. + // + // The preemption threshold can be adjusted at run time, this only sets the + // initial threshold. + // + // Precondition: preemption_threshold <= priority + constexpr Options set_preemption_threshold(UINT preemption_threshold) { + PW_DASSERT(preemption_threshold < PW_THREAD_THREADX_CONFIG_MIN_PRIORITY); + possible_preemption_threshold_ = preemption_threshold; + 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. + // + // Time slicing is disabled while the preemption threshold is enabled, i.e. + // not equal to the priority, even if a time slice interval was specified. + // + // A value of TX_NO_TIME_SLICE (a value of 0) disables time-slicing of this + // thread. + // + // Using time slicing results in a slight amount of system overhead, threads + // with a unique priority should consider TX_NO_TIME_SLICE. + constexpr Options set_time_slice_interval(ULONG time_slice_interval) { + time_slice_interval_ = time_slice_interval; + return *this; + } + + // Set the pre-allocated context (all memory needed to run a thread), see the + // pw::thread::threadx::Context for more detail. + constexpr Options set_context(Context& context) { + context_ = &context; + return *this; + } + + private: + friend thread::Thread; + // Note that the default name may end up truncated due to + // PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN. + static constexpr char kDefaultName[] = "pw::Thread"; + + const char* name() const { return name_; } + UINT priority() const { return priority_; } + UINT preemption_threshold() const { + return possible_preemption_threshold_.value_or(priority_); + } + ULONG time_slice_interval() const { return time_slice_interval_; } + Context* context() const { return context_; } + + const char* name_ = kDefaultName; + UINT priority_ = config::kDefaultPriority; + // A default value cannot be used for the preemption threshold as it would + // have to be based on the selected priority. + std::optional<UINT> possible_preemption_threshold_ = std::nullopt; + ULONG time_slice_interval_ = config::kDefaultTimeSliceInterval; + Context* context_ = nullptr; +}; + +} // namespace pw::thread::threadx diff --git a/pw_thread_threadx/public/pw_thread_threadx/thread_inline.h b/pw_thread_threadx/public/pw_thread_threadx/thread_inline.h new file mode 100644 index 000000000..33ab70f27 --- /dev/null +++ b/pw_thread_threadx/public/pw_thread_threadx/thread_inline.h @@ -0,0 +1,50 @@ +// 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_threadx/config.h" +#include "pw_thread_threadx/options.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_threadx/public/pw_thread_threadx/thread_native.h b/pw_thread_threadx/public/pw_thread_threadx/thread_native.h new file mode 100644 index 000000000..3fb42feaf --- /dev/null +++ b/pw_thread_threadx/public/pw_thread_threadx/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_threadx/context.h" + +namespace pw::thread::backend { + +// The native thread is a pointer to a thread's context. +using NativeThread = pw::thread::threadx::Context*; + +// The native thread handle is the same as the NativeThread. +using NativeThreadHandle = pw::thread::threadx::Context*; + +} // namespace pw::thread::backend diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_threadx/public_overrides/pw_thread_backend/thread_inline.h new file mode 100644 index 000000000..de91c73d7 --- /dev/null +++ b/pw_thread_threadx/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_threadx/thread_inline.h" diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/thread_native.h b/pw_thread_threadx/public_overrides/pw_thread_backend/thread_native.h new file mode 100644 index 000000000..dfa8d2174 --- /dev/null +++ b/pw_thread_threadx/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_threadx/thread_native.h" diff --git a/pw_thread_threadx/test_threads.cc b/pw_thread_threadx/test_threads.cc new file mode 100644 index 000000000..0cf222b56 --- /dev/null +++ b/pw_thread_threadx/test_threads.cc @@ -0,0 +1,72 @@ +// 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 "pw_assert/check.h" +#include "pw_chrono/system_clock.h" +#include "pw_log/log.h" +#include "pw_thread/sleep.h" +#include "pw_thread_threadx/context.h" +#include "pw_thread_threadx/options.h" + +namespace pw::thread::test { +namespace { + +std::array<threadx::ContextWithStack<>, 2> thread_contexts; + +} // namespace + +const Options& TestOptionsThread0() { + static constexpr threadx::Options thread_0_options = + threadx::Options() + .set_name("pw::TestThread0") + .set_context(thread_contexts[0]); + return thread_0_options; +} + +const Options& TestOptionsThread1() { + static constexpr threadx::Options thread_1_options = + threadx::Options() + .set_name("pw::TestThread1") + .set_context(thread_contexts[1]); + return thread_1_options; +} + +void WaitUntilDetachedThreadsCleanedUp() { + // ThreadX does not permit the running thread to delete itself, which means + // we have to do this to re-use a TCB as otherwise we will be leaking stale + // references in the kernel. + for (auto& context : thread_contexts) { + if (context.tcb().tx_thread_id != TX_THREAD_ID) { + // The TCB was either not used or was already deleted. Note that + // tx_thread_terminate does NOT clear this state by design. + continue; + } + + // If the thread was created but has not been deleted, it means that the + // thread was detached before it finished. Wait until it is completed. + while (context.tcb().tx_thread_state != TX_COMPLETED) { + pw::this_thread::sleep_for( + chrono::SystemClock::for_at_least(std::chrono::milliseconds(1))); + } + + const UINT result = tx_thread_delete(&context.tcb()); + PW_CHECK_UINT_EQ(TX_SUCCESS, result, "Failed to delete thread"); + } +} + +} // namespace pw::thread::test diff --git a/pw_thread_threadx/thread.cc b/pw_thread_threadx/thread.cc new file mode 100644 index 000000000..72befae66 --- /dev/null +++ b/pw_thread_threadx/thread.cc @@ -0,0 +1,201 @@ +// 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/assert.h" +#include "pw_preprocessor/compiler.h" +#include "pw_thread/id.h" +#include "pw_thread_threadx/config.h" +#include "pw_thread_threadx/context.h" +#include "pw_thread_threadx/options.h" +#include "tx_event_flags.h" + +using pw::thread::threadx::Context; + +namespace pw::thread { +namespace { +#if PW_THREAD_JOINING_ENABLED +constexpr ULONG kThreadDoneBit = 1; +#endif // PW_THREAD_JOINING_ENABLED +} // namespace + +void Context::RunThread(ULONG void_context_ptr) { + Context& context = *reinterpret_cast<Context*>(void_context_ptr); + context.entry_(context.arg_); + + // Raise our preemption threshold as a thread only critical section to guard + // against join() and detach(). + UINT original_preemption_threshold = TX_MAX_PRIORITIES; // Invalid. + UINT preemption_success = tx_thread_preemption_change( + &context.tcb(), 0, &original_preemption_threshold); + PW_DCHECK_UINT_EQ(TX_SUCCESS, + preemption_success, + "Failed to enter thread critical section"); + if (context.detached()) { + // There is no threadsafe way to re-use detached threads, as there's no way + // to invoke tx_thread_delete() from the running thread! Joining MUST be + // used for this. However to enable unit test coverage we go ahead and clear + // this. + 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. + const UINT event_group_result = + tx_event_flags_delete(&context.join_event_group()); + PW_DCHECK_UINT_EQ(TX_SUCCESS, + event_group_result, + "Failed to delete the join event group"); +#endif // PW_THREAD_JOINING_ENABLED + + // Note that we do not have to restore our preemption threshold as this + // thread is completing execution. + + // WARNING: The thread at this point continues to be registered with the + // kernel in TX_COMPLETED state, as tx_thread_delete cannot be invoked! + return; + } + + // Otherwise the task finished before the thread was detached or joined, defer + // cleanup to Thread's join() or detach(). + context.set_thread_done(); + UINT unused = 0; + preemption_success = tx_thread_preemption_change( + &context.tcb(), original_preemption_threshold, &unused); + PW_DCHECK_UINT_EQ(TX_SUCCESS, + preemption_success, + "Failed to leave thread critical section"); + +#if PW_THREAD_JOINING_ENABLED + const UINT result = + tx_event_flags_set(&context.join_event_group(), kThreadDoneBit, TX_OR); + PW_DCHECK_UINT_EQ(TX_SUCCESS, result, "Failed to set the join event"); +#endif // PW_THREAD_JOINING_ENABLED + return; +} + +void Context::DeleteThread(Context& context) { + // Stop the other task first. + UINT thread_result = tx_thread_terminate(&context.tcb()); + PW_CHECK_UINT_EQ(TX_SUCCESS, thread_result, "Failed to terminate the thread"); + + // Delete the thread, removing it out of the kernel. + thread_result = tx_thread_delete(&context.tcb()); + PW_CHECK_UINT_EQ(TX_SUCCESS, thread_result, "Failed to delete the thread"); + + // 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. + const UINT event_group_result = + tx_event_flags_delete(&context.join_event_group()); + PW_DCHECK_UINT_EQ( + TX_SUCCESS, event_group_result, "Failed to delete the join event group"); +#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 threadx::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(false); + native_type_->set_detached(false); + native_type_->set_thread_done(false); +#if PW_THREAD_JOINING_ENABLED + static const char* join_event_group_name = "pw::Thread"; + const UINT event_group_result = + tx_event_flags_create(&options.context()->join_event_group(), + const_cast<char*>(join_event_group_name)); + PW_DCHECK_UINT_EQ( + TX_SUCCESS, event_group_result, "Failed to create the join event group"); +#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); + + const UINT thread_result = + tx_thread_create(&options.context()->tcb(), + const_cast<char*>(native_type_->name()), + Context::RunThread, + reinterpret_cast<ULONG>(native_type_), + options.context()->stack().data(), + options.context()->stack().size_bytes(), + options.priority(), + options.preemption_threshold(), + options.time_slice_interval(), + TX_AUTO_START); + PW_CHECK_UINT_EQ(TX_SUCCESS, thread_result, "Failed to create the thread"); +} + +void Thread::detach() { + PW_CHECK(joinable()); + + tx_thread_suspend(&native_type_->tcb()); + native_type_->set_detached(); + const bool thread_done = native_type_->thread_done(); + tx_thread_resume(&native_type_->tcb()); + + if (thread_done) { + // The task finished (hit end of Context::RunThread) before we invoked + // detach, clean up the thread. + Context::DeleteThread(*native_type_); + } else { + // We're detaching before the task finished, defer cleanup to the task at + // the end of Context::RunThread. + } + + // 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()); + + ULONG actual_flags = 0; + const UINT result = tx_event_flags_get(&native_type_->join_event_group(), + kThreadDoneBit, + TX_OR_CLEAR, + &actual_flags, + TX_WAIT_FOREVER); + PW_DCHECK_UINT_EQ(TX_SUCCESS, result, "Failed to get the join event"); + + // No need for a critical section here as the thread at this point is + // waiting to be deleted. + Context::DeleteThread(*native_type_); + + // Update to no longer represent a thread of execution. + native_type_ = nullptr; +} +#endif // PW_THREAD_JOINING_ENABLED + +} // namespace pw::thread |