diff --git a/libraries/FilterUtils/CMakeLists.txt b/libraries/FilterUtils/CMakeLists.txt new file mode 100644 index 00000000..2de5be39 --- /dev/null +++ b/libraries/FilterUtils/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.19) +cmake_policy(VERSION 3.19) + +set(CMAKE_CXX_STANDARD 17) + +project(filter_utils LANGUAGES CXX) + +add_definitions( + -DDEVICE_ANALOGIN=1 +) + +add_subdirectory(../../mbed-os mbed-os) + +add_library(filter_utils STATIC + debounced_digital_in.cpp + filtered_analog_in.cpp +) + +target_include_directories(filter_utils PUBLIC include) + +target_link_libraries(filter_utils PUBLIC mbed-core-flags) \ No newline at end of file diff --git a/libraries/FilterUtils/README.md b/libraries/FilterUtils/README.md new file mode 100644 index 00000000..123da2aa --- /dev/null +++ b/libraries/FilterUtils/README.md @@ -0,0 +1,184 @@ +# SensorFilters + +This library provides classes that filter inputs to increase reliability and robustness. + +## Classes + +___ + +### DebouncedDigitalIn + +Debounces a digital input. + +#### Implementation +Digital inputs are debounced by using a class-wide timer to sample all the input at a high frequency (1ms), and only updates the debounced state of each pin when the signal has changed for a set number of samples (_valid_read_count). + +The signal must change for a time equal to the sampling interval multiplied by the required sample count, for example, with default values, 5 samples are required, so a signal must change for 5ms straight to be registered. If the signal is read to be the same as the current default state, then the counted number of changed reads is reset back to 0. + +If you pass in values <= 1 as the valid_read_count, then the DebouncedDigitalIn will act largely the same as a normal DigitalIn, just updating every ms rather than on every read. If, for example, you set valid_read_count to 3, then the DebouncedDigitalIn object will need to read a changed value 3 times in a row for the returned default state to then change. + +The default state for a DebouncedDigitalIn object is false (0). + +#### Code Sample +```cpp +#include "mbed.h" +#include "DebouncedDigitalIn.h" + +// Initializes a normal DigitalIn to pin PA_6. +DigitalIn button(PA_6); + +// Passes in a reference to the DigitalIn and sets the valid read count to 5. +// The valid read count is simply the amount of different reads required in +// a row for the debounced_in to return the different value. With our approach, +// each DebouncedDigitalIn is rechecked every ms at the same time. This means +// that the valid read count is the amount of ms that the signal needs to +// change for a different value to be returned (HIGH -> LOW or LOW -> HIGH). +// (See: https://my.eng.utah.edu/~cs5780/debouncing.pdf, Sections: Software +// Debouncers-A Counting Algorithm) +DebouncedDigitalIn debounced_button(button, 5); + +int main() { + // Sets the DebouncedDigitalIn object's valid read count to 7 (there must + // be a changed state for 7ms for the returned value of read() to change) + debounced_button.set_valid_read_count(7); + + while (true) { + // Runs the default read() function on the DigitalIn. + const bool default_button_state = button.read(); + + // Returns what the current debounced state is. Keep in mind that the + // read() does NOT run an update to the current debounced state since + // that happens at the same time for all objects every ms. + const bool debounced_button_state = debounced_button.read(); + } + + return 0; +} +``` + +___ + +### FilteredAnalogIn + +Smooths an analog signal like a first-pass RC filter. + +#### Implementation +The FilteredAnalogIn class implements an Exponential Weighted Moving Average (EWMA) filter to smooth out erratic analog inputs by applying a kind of digital low pass filter. + +The EWMA filter works according to the following formula: +
+
+ewma-formula +
+
+Where: +- Vsmoothed​(t) is the new smoothed value (_smoothed_value). +- Vraw​(t) is the current raw reading (raw_value from _analog_pin.read()). +- Vsmoothed​(t−Δt) is the previous smoothed value. +- $\alpha$ is the smoothing factor (or weight). + +$\alpha$, the smoothing factor is based on the time constant (_time_constant) and time difference between readings. $\alpha$ is derived as follows: +
+
+alpha-formula +
+
+This way, the cutoff frequency, which is used in determining the time constant as per the equation below, stays constant even if the time between reads is not consistent. Using the cutoff frequency in this way allows the digital filtering to behave the same as a first-pass RC filter would. +
+
+time-constant-formula +
+
+The time constant 𝜏 is the time required for the filter's output to reach ≈ 70.7% of the final value after a sudden step change. A larger time constant results in more smoothing but a slower response, while a smaller time constant results in less smoothing but a faster response. + +Here's a graph of a test run which shows how effective this method is at filtering noisy analog sensor input: +
+
+![ewma-graph-resize](https://github.com/user-attachments/assets/ff50976d-4ede-44f0-9e32-5e186e9443f4) +
+ +#### Other Methods +Two other options for smoothing are Rolling Average and Exponential Moving Average. +
+
+RA vs EMA vs EWMA +- Rolling Average: Yellow + - Treats all data points within a fixed window equally. + - Causes a lag where smoothed output is slow to react to a sudden change in sensor reading (step change). + - Requires allocated memory to properly make windows. + + +- Exponential Moving Average: Blue + - Uses an exponentially decreasing weighting factor ($\alpha$) for older observations. Recent data points contribute more significantly to the average than data points further in the past. + - Reduces lag compared to the Rolling Average because it reacts faster to recent changes. + - Sampling time variance can cause issues since all reads are treated the same. + + +- Exponential Weighted Moving Average: Purple + - The approach that the FilteredAnalogIn objects use. + - An exponential decaying weight is implemented to past data points. Same fundamental weighting scheme as the EMA. + - Uses the same filtering as a First-Order Low-Pass Filter, where the -3db level is at the cutoff frequency. + - Low-Pass Filter: Allows low-frequency signals (the underlying trend) to pass through, while attenuating high-frequency signals (noise or rapid fluctuations). + - Keep in mind that the -3db level is where ~70.7% of the signals at that frequency pass through, following a decreasing pattern past that point. +##### Code Sample 1 +```cpp +#include "mbed.h" +#include "FilteredAnalogIn.h" + +// Initializes a normal AnalogIn to pin PA_6. +AnalogIn in(PA_6); + +// Passes in a reference to the AnalogIn and sets the cutoff_frequency to 10Hz. +// The cutoff frequency is similar to a first-pass RC filter, where the frequency +// given is put at the -3db level. +// (See: https://www.electronics-tutorials.ws/filter/filter_2.html, Sections: RC +// Time Constant & Frequency Response of a 1st-order Low Pass Filter) +FilteredAnalogIn smoothed_in(in, 10); + +int main() { + // Sets the FilteredAnalogIn object's cutoff frequency to 100Hz. + smoothed_in.set_time_constant(100); + // Sets the referenced AnalogIn ("in" for this case) to have a + // reference_voltage of 3.3V. + smoothed_in.set_reference_voltage(3.3); + + while (true) { + // Runs the default read_voltage() function on the AnalogIn. + const float default_voltage = in.read_voltage(); + + // Runs the EWMA-smoothing algorithm on the current read value and returns + // the smoothed read as a result. + const float smoothed_voltage = smoothed_in.read_voltage(); + } + + return 0; +} +``` + +##### Code Sample 2 +```cpp +#include "mbed.h" +#include "FilteredAnalogIn.h" + +// Initializes a normal AnalogIn to pin PA_7. +AnalogIn temp_sensor(PA_7); + +FilteredAnalogIn smoothed_temp(temp_sensor, 2); + +int main(){ + // Set time constant to 0.1 secounds (𝜏 = 0.1s) + // Also equivalent to a cutoff frequency of fc = 1/(2*π*0.1) = 1.59 Hz. + smoothed_temp.set_time_constant(0.1); + + while(true){ + // Read current smoothed value from 0.0-1.0 range. + const float smoothed_value = smoothed_temp.read(); + // Smoothed value used for calculations and logging. + // To calculate temp from 0-1 range: + // const float current_temp = smoothed_value * 100.0f; + } + return 0; +} +``` +___ + diff --git a/libraries/FilterUtils/debounced_digital_in.cpp b/libraries/FilterUtils/debounced_digital_in.cpp new file mode 100644 index 00000000..3059b84a --- /dev/null +++ b/libraries/FilterUtils/debounced_digital_in.cpp @@ -0,0 +1,31 @@ +// +// Created by Jackson Pinsonneault on 10/27/25. +// + +#include "debounced_digital_in.h" + +int DebouncedDigitalIn::read() const { + return _current_state; +} + +int DebouncedDigitalIn::is_connected() const { + return _digital_pin.is_connected(); +} + +void DebouncedDigitalIn::set_valid_read_count(const uint16_t valid_read_count) { + _valid_read_count = valid_read_count; +} + +void DebouncedDigitalIn::add_sample() { + if (_digital_pin.read() == _current_state) { + _changed_state_time = 0; + return; + } + + _changed_state_time++; + + if (_changed_state_time >= _valid_read_count) { + _current_state = !_current_state; + _changed_state_time = 0; + } +} \ No newline at end of file diff --git a/libraries/FilterUtils/filtered_analog_in.cpp b/libraries/FilterUtils/filtered_analog_in.cpp new file mode 100644 index 00000000..00a52ad5 --- /dev/null +++ b/libraries/FilterUtils/filtered_analog_in.cpp @@ -0,0 +1,43 @@ +// +// Created by Jackson Pinsonneault on 10/28/25. +// + +#include "filtered_analog_in.h" +#include + +float FilteredAnalogIn::read() { + const unsigned long time_dif = chrono::duration_cast(_timer.elapsed_time()).count(); + const float raw_value = _analog_pin.read(); + _timer.reset(); + + if (!_is_initialized) { + _smoothed_value = raw_value; + _is_initialized = true; + } else { + const float exponent = -1.0 * static_cast(time_dif) / pow(10,6) / _time_constant; + const float exponential_component = exp(exponent); + _smoothed_value = (1 - exponential_component) * raw_value + exponential_component * _smoothed_value; + } + + return _smoothed_value; +} + +unsigned short FilteredAnalogIn::read_u16() { + return static_cast(0xFFFF * read()); +} + +float FilteredAnalogIn::read_voltage() { + return read() * _analog_pin.get_reference_voltage(); +} + +void FilteredAnalogIn::set_reference_voltage(float vref) const { + _analog_pin.set_reference_voltage(vref); +} + +float FilteredAnalogIn::get_reference_voltage() const { + return _analog_pin.get_reference_voltage(); +} + +void FilteredAnalogIn::set_time_constant(const float cutoff_frequency) { + _time_constant = 1.0 / (2.0 * M_PI * cutoff_frequency); +} \ No newline at end of file diff --git a/libraries/FilterUtils/include/debounced_digital_in.h b/libraries/FilterUtils/include/debounced_digital_in.h new file mode 100644 index 00000000..faf749e3 --- /dev/null +++ b/libraries/FilterUtils/include/debounced_digital_in.h @@ -0,0 +1,72 @@ +// +// Created by Jackson Pinsonneault on 10/27/25. +// + +#ifndef LIBRARIES_DEBOUNCE_H +#define LIBRARIES_DEBOUNCE_H + +#include +#include "mbed.h" + +class DebouncedDigitalIn; +inline void sample_all_digital_pins(); + +inline Ticker debounce_ticker; +inline bool ticker_started = false; +inline std::vector digital_pins; + +class DebouncedDigitalIn { + +public: + /** Create an auto-debouncing DigitalIn + * + * @param digital_pin Reference to a DigitalIn to use for reading + * @param valid_read_count (optional) Amount of consecutive reads of the same value needed to change what read() returns (defaults to 5) + */ + explicit DebouncedDigitalIn(DigitalIn &digital_pin, const uint16_t valid_read_count=5) : + _digital_pin(digital_pin), + _valid_read_count(valid_read_count) { + digital_pins.push_back(this); + + if (!ticker_started) { + debounce_ticker.attach(&sample_all_digital_pins, 1ms); + ticker_started = true; + } + } + + /** Read the debounced referenced DigitalIn input, represented as 0 or 1 (int) + * + * @returns An integer representing the debounced state of the referenced DigitalIn input, 0 for logical 0, 1 for logical 1 + */ + int read() const; + + /** Return the referenced DigitalIn's output setting, represented as 0 or 1 (int) + * + * @returns Non-zero value if referenced pin is connected to uc GPIO, 0 if referenced gpio object was initialized with NC + */ + int is_connected() const; + + /** Sets how resistant the debounced state is to changing + * + * @param valid_read_count Amount of consecutive reads of the same value needed to change what read() returns + */ + void set_valid_read_count(uint16_t valid_read_count); + + /** Updates the current debounced state of the referenced DigitalIn + */ + void add_sample(); + +private: + DigitalIn &_digital_pin; // DigitalIn which is used for the reads + bool _current_state = false; // current debounced state + uint16_t _valid_read_count; // amount of consecutive reads of the same value to change current_state + uint16_t _changed_state_time = 0; // how many consecutive reads (that aren't the same as current_state) have occurred +}; + +inline void sample_all_digital_pins() { + for (DebouncedDigitalIn *debounced_digital : digital_pins) { + debounced_digital->add_sample(); + } +} + +#endif //LIBRARIES_DEBOUNCE_H \ No newline at end of file diff --git a/libraries/FilterUtils/include/filtered_analog_in.h b/libraries/FilterUtils/include/filtered_analog_in.h new file mode 100644 index 00000000..94169ec1 --- /dev/null +++ b/libraries/FilterUtils/include/filtered_analog_in.h @@ -0,0 +1,77 @@ +// +// Created by Jackson Pinsonneault on 10/28/25. +// + +#ifndef MBED_OS_SMOOTHTOANALOGIN_H +#define MBED_OS_SMOOTHTOANALOGIN_H + +#include +#include "mbed.h" + +class FilteredAnalogIn { + +public: + /** Create a smoothed AnalogIn using EWMA (exponential weighted moving average) + * + * @param analog_pin Reference to an AnalogIn to use for reading + * @param cutoff_frequency Cutoff frequency at the -3db level (see ../README.md, 𝜏 = 1 / (2π * f_c)) + */ + explicit FilteredAnalogIn(AnalogIn &analog_pin, const float cutoff_frequency) : + _analog_pin(analog_pin) { + _timer.start(); + set_time_constant(cutoff_frequency); + } + + /** Read the referenced AnalogIn input voltage as an EWMA value, represented as a float in the range [0.0, 1.0] + * + * @returns A floating-point value representing the EWMA read, measured as a percentage + */ + float read(); + + /** Read the referenced AnalogIn input voltage as an EWMA value, represented as an unsigned short in the range [0x0, 0xFFFF] + * + * @returns 16-bit unsigned short representing the EWMA read, normalized to a 16-bit value + */ + unsigned short read_u16(); + + /** Read the referenced AnalogIn input voltage as an EWMA in volts. The output depends on the target board's + * ADC reference voltage (typically equal to supply voltage). The ADC reference voltage + * sets the maximum voltage the ADC can quantify (ie: ADC output == ADC_MAX_VALUE when Vin == Vref) + * + * The target's default ADC reference voltage is determined by the configuration + * option target.default-adc_vref. The reference voltage for a particular input + * can be manually specified by either the constructor or `AnalogIn::set_reference_voltage`. + * + * @returns A floating-point value representing the EWMA read, measured in volts. + */ + float read_voltage(); + + /** Sets the referenced AnalogIn's reference voltage. + * + * The AnalogIn's reference voltage is used to scale the output when calling AnalogIn::read_volts + * + * @param[in] vref New ADC reference voltage for the referenced AnalogIn. + */ + void set_reference_voltage(float vref) const; + + /** Gets the referenced AnalogIn's reference voltage. + * + * @returns A floating-point value representing the referenced AnalogIn's reference voltage, measured in volts. + */ + float get_reference_voltage() const; + + /** Changes the time constant to reflect the cutoff frequency at the -3db level + * + * @param cutoff_frequency Cutoff frequency at the -3db level (see ../README.md, 𝜏 = 1 / (2π * f_c)) + */ + void set_time_constant(float cutoff_frequency); + +private: + AnalogIn &_analog_pin; // Referenced AnalogIn which is used for reads + float _time_constant; // Time constant for RC sampling (set using cutoff frequency at the -3db level) + float _smoothed_value = 0; // The current value that should be returned by read() + bool _is_initialized = false; // States whether the EWMA has started + Timer _timer; // Find the difference in times between reads +}; + +#endif //MBED_OS_SMOOTHTOANALOGIN_H \ No newline at end of file diff --git a/libraries/README.md b/libraries/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/mbed-os/targets/TARGET_Cypress/scripts/mbed_set_post_build_cypress.cmake b/mbed-os/targets/TARGET_Cypress/scripts/mbed_set_post_build_cypress.cmake index 69c24b1e..a72cbab5 100644 --- a/mbed-os/targets/TARGET_Cypress/scripts/mbed_set_post_build_cypress.cmake +++ b/mbed-os/targets/TARGET_Cypress/scripts/mbed_set_post_build_cypress.cmake @@ -3,12 +3,6 @@ include(mbed_target_functions) -# Make sure we have the python packages we need -mbed_check_or_install_python_package(HAVE_PYTHON_CYSECURETOOLS cysecuretools "cysecuretools~=6.0") -if(NOT HAVE_PYTHON_CYSECURETOOLS) - message(FATAL_ERROR "Python package required for signing not found.") -endif() - # # Merge Cortex-M4 HEX and a Cortex-M0 HEX. # @@ -68,6 +62,13 @@ macro(mbed_post_build_psoc6_sign_image cortex_m0_hex ) if("${cypress_psoc6_target}" STREQUAL "${MBED_TARGET}") + + # Make sure we have the python packages we need + mbed_check_or_install_python_package(HAVE_PYTHON_CYSECURETOOLS cysecuretools "cysecuretools~=6.0") + if(NOT HAVE_PYTHON_CYSECURETOOLS) + message(FATAL_ERROR "Python package required for signing not found.") + endif() + function(mbed_post_build_function target) set(post_build_command ${Python3_EXECUTABLE} ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PSOC6.py