Skip to content

Commit d747c08

Browse files
authored
Wip low pass filter (#12)
* feat: low pass fir filter class * feat: simple low pass fir filter tests * feat: low pass filter constructor and update implementation * chore: docs * chore: update docs to be referenced * chore: remove iostream and int to string conversion
1 parent c9fe32e commit d747c08

File tree

6 files changed

+164
-4
lines changed

6 files changed

+164
-4
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ We have some zephyr rtos projects under zephyr_projects. To build/develop them y
1616
### Arduino
1717
We have some arduino projects, under arduino_firmware.
1818

19-
### common libraries
20-
Under common_libs/ we have our common libraries. These should be hardware agnostic, modular and testable.
19+
### Common Libraries
20+
Under common_libraries/ we have our common libraries. These should be hardware agnostic, modular and testable.
2121
We have an example here, that shows the general workflow for creating a common library module, including Catch2 testing.
2222

23+
1. [Low pass FIR filter](common_libraries/low_pass_fir_filter/README.md)
24+
2325
### Testing
2426
We use Catch2 for testing cpp and c code. It's an easy to use, and easy to integrate tool for cpp unit tests.
2527
Ideally, all our code has test coverage. Test driven development (TDD) is a powerful process where if done perfectly, you never push a bug that would impact system operations, because your tests would cover every needed operation of the system.

common_libraries/CMakeLists.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ set(BUILD_DIR ${CMAKE_CURRENT_LIST_DIR}/build)
1313

1414
set(COMMON_LIB_SOURCES
1515
${CMAKE_CURRENT_LIST_DIR}/example/some_lib.cpp
16-
16+
${CMAKE_CURRENT_LIST_DIR}/low_pass_fir_filter/low_pass_fir_filter.cpp
1717
# Autogenerated files:
1818
# ${BUILD_DIR}/autogen/*.cpp
1919
# ${BUILD_DIR}/autogen/*.c
@@ -23,10 +23,11 @@ add_library(common_lib STATIC ${COMMON_LIB_SOURCES})
2323

2424
target_include_directories(common_lib PUBLIC
2525
${CMAKE_CURRENT_LIST_DIR}/example/
26+
${CMAKE_CURRENT_LIST_DIR}/low_pass_fir_filter/
2627
)
2728

2829
add_executable(common_lib_tests
29-
${CMAKE_CURRENT_LIST_DIR}/example/test/test_some_lib.cpp)
30+
${CMAKE_CURRENT_LIST_DIR}/example/test/test_some_lib.cpp ${CMAKE_CURRENT_LIST_DIR}/low_pass_fir_filter/test/test_low_pass_fir_filter.cpp)
3031

3132

3233
target_link_libraries(common_lib_tests PRIVATE
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Low Pass FIR Filter
2+
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:
3+
```cpp
4+
#include <low_pass_fir_filter.hpp>
5+
6+
LowPassFIRFilter filter;
7+
8+
void process_incoming_data(unsigned int input) {
9+
unsigned int filtered_input = filter.update(input);
10+
send_filtered_data_to_arm(filtered_input);
11+
}
12+
```
13+
14+
1. Constructor: defaults to order of 1
15+
2. Buffer: used to store previous data points
16+
3. Update function:
17+
- 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).
18+
- Uses sliding window to calculate the average of the correct set of data points.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#include "low_pass_fir_filter.hpp"
2+
#include <stdexcept>
3+
#include <cstdio>
4+
5+
LowPassFIRFilter::LowPassFIRFilter() : LowPassFIRFilter(1) {}
6+
7+
LowPassFIRFilter::LowPassFIRFilter(unsigned int order)
8+
: buffer_is_empty(true), buffer(), buffer_index(0), order(order),
9+
coefficient(1.0f / (order + 1)), output(0) {
10+
// Valid input check
11+
if (order < 1 || order > MAX_LP_FIR_ORDER) {
12+
throw std::invalid_argument("order must be at least 1 and less than the max order");
13+
}
14+
}
15+
16+
unsigned int LowPassFIRFilter::update(unsigned int input) {
17+
if (this->buffer_is_empty) {
18+
// fill entire buffer with the first input
19+
for (int i = 0; i < this->order + 1; i++) {
20+
this->buffer[i] = input;
21+
}
22+
this->buffer_is_empty = false;
23+
return this->output = input;
24+
}
25+
26+
// `input` is the new data point that is begin added. `buffer[buffer_index]` is the old data point that must be removed.
27+
this->output = this->output + input * this->coefficient - this->buffer[this->buffer_index] * this->coefficient;
28+
this->buffer[this->buffer_index] = input;
29+
30+
// the index must wrap around once it reaches the right most side of the buffer. Note that buffer size is order + 1.
31+
this->buffer_index = (this->buffer_index + 1) % (this->order + 1);
32+
33+
return this->output;
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#ifndef FIR_FILTER_HPP
2+
#define FIR_FILTER_HPP
3+
#define MAX_LP_FIR_ORDER (10)
4+
5+
/*
6+
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.
7+
8+
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).
9+
10+
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).
11+
*/
12+
13+
class LowPassFIRFilter {
14+
private:
15+
bool buffer_is_empty;
16+
unsigned int buffer[MAX_LP_FIR_ORDER];
17+
unsigned int buffer_index;
18+
float coefficient;
19+
float output;
20+
21+
public:
22+
unsigned int order;
23+
// Default constructor (order 1, equal weights)
24+
LowPassFIRFilter();
25+
26+
// Constructor with filter order (uses equal weights)
27+
// order = number of previous inputs to use
28+
LowPassFIRFilter(unsigned int order);
29+
30+
// Update filter with new input and return filtered output
31+
unsigned int update(unsigned int input);
32+
};
33+
34+
#endif
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#define CATCH_CONFIG_MAIN // Let catch2 handle the main function and boiler plate code.
2+
#include <catch2/catch_all.hpp>
3+
#include <low_pass_fir_filter.hpp>
4+
5+
6+
TEST_CASE("test_constructor")
7+
{
8+
9+
SECTION("test_no_order")
10+
{
11+
LowPassFIRFilter filter_instance;
12+
REQUIRE(filter_instance.order == 1);
13+
}
14+
15+
SECTION("valid_orders")
16+
{
17+
LowPassFIRFilter filter_instance_1(1);
18+
REQUIRE(filter_instance_1.order == 1);
19+
20+
LowPassFIRFilter filter_instance_5(5);
21+
REQUIRE(filter_instance_5.order == 5);
22+
23+
LowPassFIRFilter filter_instance_10(MAX_LP_FIR_ORDER);
24+
REQUIRE(filter_instance_10.order == MAX_LP_FIR_ORDER);
25+
}
26+
27+
SECTION("test_order_too_low_or_too_high")
28+
{
29+
REQUIRE_THROWS_AS(LowPassFIRFilter(0), std::invalid_argument);
30+
REQUIRE_THROWS_AS(LowPassFIRFilter(MAX_LP_FIR_ORDER + 1), std::invalid_argument);
31+
}
32+
33+
}
34+
35+
TEST_CASE("test_update")
36+
{
37+
SECTION("test_empty_buffer_same_input") {
38+
LowPassFIRFilter filter_instance(5);
39+
unsigned int val = 50;
40+
for (unsigned int i = 0; i < 7; i++) {
41+
REQUIRE(filter_instance.update(val) == 50);
42+
}
43+
}
44+
45+
/*
46+
buffer = [50, 50, 50, 50, 50] -> output = 50
47+
buffer = [100, 50, 50, 50, 50] -> output = 60
48+
buffer = [100, 20, 50, 50, 50] -> output = 54
49+
*/
50+
SECTION("test_empty_buffer_different_inputs") {
51+
LowPassFIRFilter filter_instance(4);
52+
unsigned int inputs[] = {50, 50, 50, 100, 20};
53+
unsigned int expected[] = {50, 50, 50, 60, 54};
54+
unsigned int n = 5;
55+
for (unsigned int i = 0; i < n; i++) {
56+
REQUIRE(filter_instance.update(inputs[i]) == expected[i]);
57+
}
58+
}
59+
60+
61+
SECTION("test_input_spikes") {
62+
LowPassFIRFilter filter_instance(1);
63+
unsigned int inputs[] = {50, 60, 70, 500, 100, 60, 50, 40, 50, 40, 50};
64+
unsigned int expected[] = {50, 55, 65, 285, 300, 80, 55, 45, 45, 45, 45};
65+
66+
unsigned int n = 11;
67+
for (unsigned int i = 0; i < n; i++) {
68+
REQUIRE(filter_instance.update(inputs[i]) == expected[i]);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)