-
Notifications
You must be signed in to change notification settings - Fork 0
Wip low pass filter #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
1268a53
feat: low pass fir filter class
douglasichen 68bc753
feat: simple low pass fir filter tests
douglasichen a65821c
feat: low pass filter constructor and update implementation
douglasichen 47eb2b9
chore: docs
douglasichen eba443b
chore: update docs to be referenced
douglasichen 6297d9e
chore: remove iostream and int to string conversion
douglasichen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Low Pass FIR Filter | ||
| This is an FIR filter that can be used on any unsigned 32-bit integers. You can specify the order (AKA. the number of previous data points the algorithm considers) when creating an instance of LowPassFIRFilter (default order of 1). The following example filters incoming data with an order of 1: | ||
| ```cpp | ||
| #include <low_pass_fir_filter.hpp> | ||
|
|
||
| LowPassFIRFilter filter; | ||
|
|
||
| void process_incoming_data(unsigned int input) { | ||
| unsigned int filtered_input = filter.update(input); | ||
| send_filtered_data_to_arm(filtered_input); | ||
| } | ||
| ``` | ||
|
|
||
| 1. Constructor: defaults to order of 1 | ||
| 2. Buffer: used to store previous data points | ||
| 3. Update function: | ||
| - Fills the entire buffer with the first input to simplify the algorithm (otherwise cases for when the buffer is not full yet have to be handled differently). | ||
| - Uses sliding window to calculate the average of the correct set of data points. |
34 changes: 34 additions & 0 deletions
34
common_libraries/low_pass_fir_filter/low_pass_fir_filter.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #include "low_pass_fir_filter.hpp" | ||
| #include <stdexcept> | ||
| #include <cstdio> | ||
|
|
||
| LowPassFIRFilter::LowPassFIRFilter() : LowPassFIRFilter(1) {} | ||
|
|
||
| LowPassFIRFilter::LowPassFIRFilter(unsigned int order) | ||
| : buffer_is_empty(true), buffer(), buffer_index(0), order(order), | ||
| coefficient(1.0f / (order + 1)), output(0) { | ||
| // Valid input check | ||
| if (order < 1 || order > MAX_LP_FIR_ORDER) { | ||
| throw std::invalid_argument("order must be at least 1 and less than the max order"); | ||
| } | ||
| } | ||
|
|
||
| unsigned int LowPassFIRFilter::update(unsigned int input) { | ||
| if (this->buffer_is_empty) { | ||
| // fill entire buffer with the first input | ||
| for (int i = 0; i < this->order + 1; i++) { | ||
| this->buffer[i] = input; | ||
| } | ||
| this->buffer_is_empty = false; | ||
| return this->output = input; | ||
| } | ||
|
|
||
| // `input` is the new data point that is begin added. `buffer[buffer_index]` is the old data point that must be removed. | ||
| this->output = this->output + input * this->coefficient - this->buffer[this->buffer_index] * this->coefficient; | ||
| this->buffer[this->buffer_index] = input; | ||
|
|
||
| // the index must wrap around once it reaches the right most side of the buffer. Note that buffer size is order + 1. | ||
| this->buffer_index = (this->buffer_index + 1) % (this->order + 1); | ||
|
|
||
| return this->output; | ||
| } |
34 changes: 34 additions & 0 deletions
34
common_libraries/low_pass_fir_filter/low_pass_fir_filter.hpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| #ifndef FIR_FILTER_HPP | ||
| #define FIR_FILTER_HPP | ||
| #define MAX_LP_FIR_ORDER (10) | ||
|
|
||
| /* | ||
| This class is used to apply a low pass FIR filter to a stream of inputs with a specific order. This filter is simply taking S=[ current_input, previous_input_1, previous_input_2, ... , previous_input_{order} ], and returning the average. | ||
|
|
||
| The buffer is initially filled with the first input to simplfy the algorithm (this removes the need for taking the average of less than "order" + 1 number of inputs). | ||
|
|
||
| The buffer is of size "order" + 1 since we need to store the current input, and also remember the oldest input (to remove from the total output in a subsequent update). | ||
| */ | ||
|
|
||
| class LowPassFIRFilter { | ||
| private: | ||
| bool buffer_is_empty; | ||
| unsigned int buffer[MAX_LP_FIR_ORDER]; | ||
| unsigned int buffer_index; | ||
| float coefficient; | ||
| float output; | ||
|
|
||
| public: | ||
| unsigned int order; | ||
| // Default constructor (order 1, equal weights) | ||
| LowPassFIRFilter(); | ||
|
|
||
| // Constructor with filter order (uses equal weights) | ||
| // order = number of previous inputs to use | ||
| LowPassFIRFilter(unsigned int order); | ||
|
|
||
| // Update filter with new input and return filtered output | ||
| unsigned int update(unsigned int input); | ||
| }; | ||
|
|
||
| #endif | ||
71 changes: 71 additions & 0 deletions
71
common_libraries/low_pass_fir_filter/test/test_low_pass_fir_filter.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| #define CATCH_CONFIG_MAIN // Let catch2 handle the main function and boiler plate code. | ||
| #include <catch2/catch_all.hpp> | ||
| #include <low_pass_fir_filter.hpp> | ||
|
|
||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good usage of catch2. |
||
| TEST_CASE("test_constructor") | ||
| { | ||
|
|
||
| SECTION("test_no_order") | ||
| { | ||
| LowPassFIRFilter filter_instance; | ||
| REQUIRE(filter_instance.order == 1); | ||
| } | ||
|
|
||
| SECTION("valid_orders") | ||
| { | ||
| LowPassFIRFilter filter_instance_1(1); | ||
| REQUIRE(filter_instance_1.order == 1); | ||
|
|
||
| LowPassFIRFilter filter_instance_5(5); | ||
| REQUIRE(filter_instance_5.order == 5); | ||
|
|
||
| LowPassFIRFilter filter_instance_10(MAX_LP_FIR_ORDER); | ||
| REQUIRE(filter_instance_10.order == MAX_LP_FIR_ORDER); | ||
| } | ||
|
|
||
| SECTION("test_order_too_low_or_too_high") | ||
| { | ||
| REQUIRE_THROWS_AS(LowPassFIRFilter(0), std::invalid_argument); | ||
| REQUIRE_THROWS_AS(LowPassFIRFilter(MAX_LP_FIR_ORDER + 1), std::invalid_argument); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| TEST_CASE("test_update") | ||
| { | ||
| SECTION("test_empty_buffer_same_input") { | ||
| LowPassFIRFilter filter_instance(5); | ||
| unsigned int val = 50; | ||
| for (unsigned int i = 0; i < 7; i++) { | ||
| REQUIRE(filter_instance.update(val) == 50); | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| buffer = [50, 50, 50, 50, 50] -> output = 50 | ||
| buffer = [100, 50, 50, 50, 50] -> output = 60 | ||
| buffer = [100, 20, 50, 50, 50] -> output = 54 | ||
| */ | ||
| SECTION("test_empty_buffer_different_inputs") { | ||
| LowPassFIRFilter filter_instance(4); | ||
| unsigned int inputs[] = {50, 50, 50, 100, 20}; | ||
| unsigned int expected[] = {50, 50, 50, 60, 54}; | ||
| unsigned int n = 5; | ||
| for (unsigned int i = 0; i < n; i++) { | ||
| REQUIRE(filter_instance.update(inputs[i]) == expected[i]); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| SECTION("test_input_spikes") { | ||
| LowPassFIRFilter filter_instance(1); | ||
| unsigned int inputs[] = {50, 60, 70, 500, 100, 60, 50, 40, 50, 40, 50}; | ||
| unsigned int expected[] = {50, 55, 65, 285, 300, 80, 55, 45, 45, 45, 45}; | ||
|
|
||
| unsigned int n = 11; | ||
| for (unsigned int i = 0; i < n; i++) { | ||
| REQUIRE(filter_instance.update(inputs[i]) == expected[i]); | ||
| } | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK this would allocate more memory than needed if user does not use max order.
Great use case for a template...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm used to prioritizing speed over space (static allocation reduces overhead a bit - compared to dynamically allocating vector at runtime), is space usually more important for firmware?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good question, but here a template will still be static allocation at compile time. I might be missing a use case so take this with a grain of salt, but regardless of a template or a hard coded buffer size its still up to the user to allocate the memory at runtime or compile time.
A vector also uses templates, but the template in a vector determines the type of data stored, not the size of the vector.
in my experience it depends on the project/team and micro-controller, but in robotics firmware i think speed is usually more important to prioritize. In other words, I've found its usually easier to allocate more memory without causing an issue compared to taking up more cpu time. Dynamic allocation on the heap is also just more prone to errors on a micro controller compared to a 'full computer' so always good to bias towards static allocation in firmware.
Sorry for the wordy explanation, happy to talk about this more over a call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had trouble creating a template that could be used across both hpp and cpp files. I left this out for now.