Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PROVESFlightControllerReference/Components/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PayloadCom/")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PowerMonitor/")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ResetManager/")
#add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SBand/")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/StackMonitor/")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/StartupManager/")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ThermalManager/")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/Watchdog")
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
####
# F Prime CMakeLists.txt:
#
# SOURCES: list of source files (to be compiled)
# AUTOCODER_INPUTS: list of files to be passed to the autocoders
# DEPENDS: list of libraries that this module depends on
#
# More information in the F´ CMake API documentation:
# https://fprime.jpl.nasa.gov/latest/docs/reference/api/cmake/API/
#
####

# Module names are derived from the path from the nearest project/library/framework
# root when not specifically overridden by the developer. i.e. The module defined by
# `Ref/SignalGen/CMakeLists.txt` will be named `Ref_SignalGen`.

register_fprime_library(
AUTOCODER_INPUTS
"${CMAKE_CURRENT_LIST_DIR}/StackMonitor.fpp"
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/StackMonitor.cpp"
"${CMAKE_CURRENT_LIST_DIR}/StackMonitorCore.cpp"
# DEPENDS
# MyPackage_MyOtherModule
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// ======================================================================
// \title StackMonitor.cpp
// \brief cpp file for StackMonitor component implementation class
// ======================================================================

#include "PROVESFlightControllerReference/Components/StackMonitor/StackMonitor.hpp"

#include <zephyr/kernel.h>

namespace Components {

namespace {

//! k_thread_foreach_unlocked callback: append this thread's stack usage to
//! the ThreadStackSampleSet pointed to by userData. Called without the
//! thread-monitor spinlock held, so it must not allocate or block: it only
//! reads the stack high-water mark, does a bounded string copy of the name,
//! and stores integers into a fixed slot (ThreadStackSampleSet::add). Keeps
//! all Zephyr calls out of StackMonitorCore, which stays pure.
void appendThreadSample(const struct k_thread* thread, void* userData) {
auto* samples = static_cast<ThreadStackSampleSet*>(userData);

std::size_t unusedBytes = 0;
if (k_thread_stack_space_get(thread, &unusedBytes) != 0) {
// Couldn't read this thread's stack info (e.g. mid-teardown); skip
// it rather than report a bogus sample. The next tick will pick it
// back up if it's still alive.
return;
}

const char* name = "unknown";
#if defined(CONFIG_THREAD_NAME)
// thread is logically read-only here, but k_thread_name_get takes a
// non-const k_tid_t; the callback signature is fixed by
// k_thread_foreach_unlocked and always hands us a live thread object.
const char* threadName = k_thread_name_get(const_cast<k_tid_t>(thread));
if (threadName != nullptr) {
name = threadName;
}
#endif

(void)samples->add(name, static_cast<std::uint32_t>(thread->stack_info.size),
static_cast<std::uint32_t>(unusedBytes));
}

} // namespace

// ----------------------------------------------------------------------
// Component construction and destruction
// ----------------------------------------------------------------------

StackMonitor ::StackMonitor(const char* const compName)
: StackMonitorComponentBase(compName), m_core(WARN_THRESHOLD_PERCENT), m_samples(), m_result() {}

StackMonitor ::~StackMonitor() {}

// ----------------------------------------------------------------------
// Handler implementations for user-defined typed input ports
// ----------------------------------------------------------------------

void StackMonitor ::run_handler(FwIndexType portNum, U32 context) {
this->m_samples.clear();
k_thread_foreach_unlocked(&appendThreadSample, &this->m_samples);

this->m_core.tick(this->m_samples, this->m_result);

this->tlmWrite_MinFreeBytes(this->m_result.summary.worstThreadFreeBytes);
this->tlmWrite_WorstThread(Fw::TlmString(this->m_result.summary.worstThreadName));
this->tlmWrite_ThreadsBelowThreshold(this->m_result.summary.threadsBelowThreshold);
this->tlmWrite_SampleOverflow(this->m_result.summary.overflowed);

for (std::uint32_t i = 0; i < this->m_result.warningCount; i++) {
const StackWarning& warning = this->m_result.newWarnings[i];
this->log_WARNING_HI_StackLow(Fw::LogStringArg(warning.name), warning.freeBytes, warning.sizeBytes);
}
for (std::uint32_t i = 0; i < this->m_result.recoveryCount; i++) {
this->log_ACTIVITY_HI_StackRecovered(Fw::LogStringArg(this->m_result.newRecoveries[i].name));
}
}

} // namespace Components
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module Components {
@ Program-wide stack-usage watchdog. Walks all live Zephyr threads once per
@ tick (k_thread_foreach) and reports per-thread stack high-water
@ telemetry, warning when any thread's free stack drops below a configurable
@ percent of its own size and clearing on recovery. Fixed-capacity storage
@ throughout: zero heap allocation after construction.
@ S-Band reintegration plan, PR 1 / Slice 1.1 (D6).
passive component StackMonitor {

@ Port receiving calls from the rate group
sync input port run: Svc.Sched

@ Free bytes remaining on the thread under the most stack pressure this tick
telemetry MinFreeBytes: U32

@ Name of the thread under the most stack pressure this tick
telemetry WorstThread: string size 32

@ Count of threads currently below the warn threshold
telemetry ThreadsBelowThreshold: U32

@ True when a tick saw more live threads than the monitor's fixed
@ capacity (extra threads went unsampled that tick -- not silent)
telemetry SampleOverflow: bool

@ Event logged when a thread's free stack drops below its warn threshold
event StackLow(
thread: string size 32 @< Name of the thread
freeBytes: U32 @< Free bytes remaining on the thread's stack
sizeBytes: U32 @< Total size of the thread's stack
) \
severity warning high \
format "Thread {} stack low: {} of {} bytes free"

@ Event logged when a previously-low thread recovers above its warn threshold
event StackRecovered(
thread: string size 32 @< Name of the thread
) \
severity activity high \
format "Thread {} stack recovered"

###############################################################################
# Standard AC Ports: Required for Channels, Events, Commands, and Parameters #
###############################################################################
@ Port for requesting the current time
time get port timeCaller

@ Port for sending command registrations
command reg port cmdRegOut

@ Port for receiving commands
command recv port cmdIn

@ Port for sending command responses
command resp port cmdResponseOut

@ Port for sending textual representation of events
text event port logTextOut

@ Port for sending events to downlink
event port logOut

@ Port for sending telemetry channels to downlink
telemetry port tlmOut

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// ======================================================================
// \title StackMonitor.hpp
// \brief hpp file for StackMonitor component implementation class
// ======================================================================

#ifndef Components_StackMonitor_HPP
#define Components_StackMonitor_HPP

#include "PROVESFlightControllerReference/Components/StackMonitor/StackMonitorComponentAc.hpp"
#include "PROVESFlightControllerReference/Components/StackMonitor/StackMonitorCore.hpp"

namespace Components {

class StackMonitor final : public StackMonitorComponentBase {
public:
// ----------------------------------------------------------------------
// Component construction and destruction
// ----------------------------------------------------------------------

//! Construct StackMonitor object
StackMonitor(const char* const compName //!< The component name
);

//! Destroy StackMonitor object
~StackMonitor();

private:
// ----------------------------------------------------------------------
// Handler implementations for user-defined typed input ports
// ----------------------------------------------------------------------

//! Handler implementation for run
//!
//! Port receiving calls from the rate group
void run_handler(FwIndexType portNum, //!< The port number
U32 context //!< The call order
) override;

//! Warn when a thread's free stack drops below this percent of its own size.
static constexpr std::uint32_t WARN_THRESHOLD_PERCENT = 20;

//! Pure-logic core: turns a snapshot of per-thread stack usage into a
//! summary and warn/clear decisions. Host-testable in isolation; see
//! test/unit-tests/test_StackMonitor_Core.cpp.
StackMonitorCore m_core;

//! Per-tick sample and result storage. Fixed-size and kept as member
//! state (not run_handler locals): together they are a few KB, which
//! would not be comfortable on the 4 KB rate-group thread stack, and
//! keeping them here guarantees zero heap allocation after boot.
ThreadStackSampleSet m_samples;
StackMonitorTickResult m_result;
};

} // namespace Components

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// ======================================================================
// \title StackMonitorCore.cpp
// \brief cpp file for StackMonitorCore pure-logic class
// ======================================================================

#include "PROVESFlightControllerReference/Components/StackMonitor/StackMonitorCore.hpp"

#include <cstring>

namespace Components {

namespace {

//! Bounded copy of a thread name into a fixed-size slot, always
//! null-terminated. No allocation.
void copyName(char (&dst)[STACK_MONITOR_MAX_NAME_LEN], const char* src) {
(void)std::strncpy(dst, (src != nullptr) ? src : "", STACK_MONITOR_MAX_NAME_LEN - 1);
dst[STACK_MONITOR_MAX_NAME_LEN - 1] = '\0';
}

//! Free bytes, clamped so a corrupt/odd sample can never read as more free
//! than the thread's total stack size.
std::uint32_t clampedFreeBytes(const ThreadStackSample& sample) {
return (sample.freeBytes > sample.sizeBytes) ? sample.sizeBytes : sample.freeBytes;
}

//! Percent of the thread's stack currently free, 0-100.
std::uint32_t freePercent(const ThreadStackSample& sample) {
if (sample.sizeBytes == 0) {
return 0;
}
return (clampedFreeBytes(sample) * 100) / sample.sizeBytes;
}

} // namespace

// ----------------------------------------------------------------------
// ThreadStackSampleSet
// ----------------------------------------------------------------------

void ThreadStackSampleSet::clear() {
this->count = 0;
this->overflowed = false;
}

bool ThreadStackSampleSet::add(const char* name, std::uint32_t sizeBytes, std::uint32_t freeBytes) {
if (this->count >= STACK_MONITOR_MAX_THREADS) {
this->overflowed = true;
return false;
}
ThreadStackSample& slot = this->samples[this->count];
copyName(slot.name, name);
slot.sizeBytes = sizeBytes;
slot.freeBytes = freeBytes;
this->count++;
return true;
}

// ----------------------------------------------------------------------
// StackMonitorCore
// ----------------------------------------------------------------------

StackMonitorCore::StackMonitorCore(std::uint32_t warnThresholdPercent)
: m_warnThresholdPercent(warnThresholdPercent), m_warned() {}

void StackMonitorCore::tick(const ThreadStackSampleSet& sampleSet, StackMonitorTickResult& result) {
result.summary = StackMonitorSummary();
result.summary.overflowed = sampleSet.overflowed;
result.warningCount = 0;
result.recoveryCount = 0;

bool haveWorst = false;
std::uint32_t worstFreePercent = 0;

for (std::uint32_t i = 0; i < sampleSet.count; i++) {
const ThreadStackSample& sample = sampleSet.samples[i];
std::uint32_t fPercent = freePercent(sample);

if (!haveWorst || fPercent < worstFreePercent) {
haveWorst = true;
worstFreePercent = fPercent;
copyName(result.summary.worstThreadName, sample.name);
result.summary.worstThreadFreeBytes = sample.freeBytes;
result.summary.worstThreadUsedPercent = 100 - fPercent;
}

bool isBelowThreshold = fPercent < m_warnThresholdPercent;
if (isBelowThreshold) {
result.summary.threadsBelowThreshold++;
}

bool wasWarned = (this->findWarned(sample.name) >= 0);
if (isBelowThreshold && !wasWarned) {
if (result.warningCount < STACK_MONITOR_MAX_THREADS) {
StackWarning& warning = result.newWarnings[result.warningCount];
copyName(warning.name, sample.name);
warning.freeBytes = sample.freeBytes;
warning.sizeBytes = sample.sizeBytes;
result.warningCount++;
}
this->setWarned(sample.name);
} else if (!isBelowThreshold && wasWarned) {
if (result.recoveryCount < STACK_MONITOR_MAX_THREADS) {
copyName(result.newRecoveries[result.recoveryCount].name, sample.name);
result.recoveryCount++;
}
this->clearWarned(sample.name);
}
}
}

std::int32_t StackMonitorCore::findWarned(const char* name) const {
for (std::uint32_t i = 0; i < STACK_MONITOR_MAX_THREADS; i++) {
if (this->m_warned[i].used && (std::strncmp(this->m_warned[i].name, name, STACK_MONITOR_MAX_NAME_LEN) == 0)) {
return static_cast<std::int32_t>(i);
}
}
return -1;
}

void StackMonitorCore::setWarned(const char* name) {
if (this->findWarned(name) >= 0) {
return;
}
for (std::uint32_t i = 0; i < STACK_MONITOR_MAX_THREADS; i++) {
if (!this->m_warned[i].used) {
copyName(this->m_warned[i].name, name);
this->m_warned[i].used = true;
return;
}
}
// Table full: the warning event is still emitted by tick(); this
// thread just isn't latched (it may re-warn on a later tick).
}

void StackMonitorCore::clearWarned(const char* name) {
std::int32_t index = this->findWarned(name);
if (index >= 0) {
this->m_warned[index].used = false;
}
}

} // namespace Components
Loading