Compare commits
7 Commits
feature/im
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af9db5a1b0 | ||
|
|
470ad75376 | ||
| e6f5315dac | |||
| f64a92aa5c | |||
| 0698081d7b | |||
|
|
927b950dec | ||
|
|
47d7e87023 |
28
.drone.yml
28
.drone.yml
@ -1,28 +0,0 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: build and test
|
||||
pull: never
|
||||
image: erki/cpp-ubuntu:latest
|
||||
commands:
|
||||
- mkdir -p build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=ON
|
||||
- ninja
|
||||
- ctest . --output-on-failure
|
||||
- name: notify
|
||||
image: plugins/matrix
|
||||
settings:
|
||||
homeserver:
|
||||
from_secret: matrix_homeserver
|
||||
roomid:
|
||||
from_secret: matrix_roomid
|
||||
username:
|
||||
from_secret: matrix_username
|
||||
password:
|
||||
from_secret: matrix_password
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
41
.gitea/workflows/ci.yml
Normal file
41
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,41 @@
|
||||
name: CI & Unit Tests
|
||||
run-name: CI & Unit Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
Unit-Tests:
|
||||
runs-on: fedora-cpp
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Configure build
|
||||
working-directory: ${{runner.workspace}}
|
||||
run: |
|
||||
cmake . -B${{runner.workspace}}/build \
|
||||
-G"Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DSKULLC_WITH_TESTS=ON
|
||||
|
||||
- name: Build tests + lib
|
||||
working-directory: ${{runner.workspace}}/build
|
||||
run: ninja
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ${{runner.workspace}}/build
|
||||
run: ctest . --output-on-failure
|
||||
Docs:
|
||||
runs-on: fedora-cpp
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Configure build
|
||||
working-directory: ${{runner.workspace}}
|
||||
run: |
|
||||
cmake . -B${{runner.workspace}}/build \
|
||||
-G"Ninja" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DSKULLC_WITH_DOCS=ON
|
||||
|
||||
- name: Build docs
|
||||
working-directory: ${{runner.workspace}}/build
|
||||
run: ninja Sphinx
|
||||
@ -14,6 +14,9 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/)
|
||||
|
||||
option(SKULLC_WITH_TESTS "Enable unit testing." OFF)
|
||||
option(SKULLC_WITH_HAL "Enable the compiling and deployment of the HAL dependent sections." OFF)
|
||||
option(SKULLC_USE_HAL_ST "Enable the ST HAl when SKULLC_WITH_HAL is enabled." OFF)
|
||||
option(SKULLC_USE_HAL_ESP "Enable the ESP HAL when SKULLC_WITH_HAL is enabled." OFF)
|
||||
option(SKULLC_WITH_DOCS "Enable documentation building." OFF)
|
||||
|
||||
include(skullc-install)
|
||||
|
||||
@ -26,6 +29,10 @@ if(SKULLC_WITH_TESTS)
|
||||
add_subdirectory(Tests)
|
||||
endif()
|
||||
|
||||
if(SKULLC_WITH_DOCS)
|
||||
add_subdirectory(Docs)
|
||||
endif()
|
||||
|
||||
## Install
|
||||
|
||||
configure_file(skullc-config.cmake
|
||||
|
||||
53
Docs/CMakeLists.txt
Normal file
53
Docs/CMakeLists.txt
Normal file
@ -0,0 +1,53 @@
|
||||
find_package(Doxygen REQUIRED)
|
||||
find_package(Sphinx REQUIRED)
|
||||
|
||||
file(GLOB_RECURSE SKULLC_PUBLIC_HEADERS
|
||||
${PROJECT_SOURCE_DIR}/Messaging/Inc/*
|
||||
${PROJECT_SOURCE_DIR}/Threads/Inc/*
|
||||
${PROJECT_SOURCE_DIR}/Utility/Inc/*
|
||||
${PROJECT_SOURCE_DIR}/Peripherals/Inc/*
|
||||
)
|
||||
|
||||
set(DOXYGEN_INPUT_DIRS "${PROJECT_SOURCE_DIR}/Messaging/Inc ${PROJECT_SOURCE_DIR}/Threads/Inc ${PROJECT_SOURCE_DIR}/Utility/Inc ${PROJECT_SOURCE_DIR}/Peripherals/Inc")
|
||||
set(DOXYGEN_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen)
|
||||
set(DOXYGEN_INDEX_FILE ${DOXYGEN_OUTPUT_DIR}/xml/index.xml)
|
||||
set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in)
|
||||
set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
|
||||
|
||||
configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY)
|
||||
|
||||
file(MAKE_DIRECTORY ${DOXYGEN_OUTPUT_DIR}) #Doxygen won't create this for us
|
||||
add_custom_command(OUTPUT ${DOXYGEN_INDEX_FILE}
|
||||
DEPENDS ${SKULLC_PUBLIC_HEADERS}
|
||||
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT}
|
||||
MAIN_DEPENDENCY ${DOXYFILE_OUT} ${DOXYFILE_IN}
|
||||
COMMENT "Generating docs"
|
||||
)
|
||||
|
||||
add_custom_target(Doxygen ALL DEPENDS ${DOXYGEN_INDEX_FILE})
|
||||
|
||||
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/sphinx)
|
||||
set(SPHINX_INDEX_FILE ${SPHINX_BUILD}/index.html)
|
||||
file(GLOB SPHINX_RST_FILES ${SPHINX_SOURCE}/*.rst)
|
||||
|
||||
# Only regenerate Sphinx when:
|
||||
# - Doxygen has rerun
|
||||
# - Our doc files have been updated
|
||||
# - The Sphinx config has been updated
|
||||
add_custom_command(OUTPUT ${SPHINX_INDEX_FILE}
|
||||
COMMAND
|
||||
${SPHINX_EXECUTABLE} -b html
|
||||
# Tell Breathe where to find the Doxygen output
|
||||
-Dbreathe_projects.SkullCPeripheralsLibrary=${DOXYGEN_OUTPUT_DIR}/xml
|
||||
${SPHINX_SOURCE} ${SPHINX_BUILD}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
DEPENDS
|
||||
# Other docs files you want to track should go here (or in some variable)
|
||||
${SPHINX_RST_FILES}
|
||||
${DOXYGEN_INDEX_FILE}
|
||||
MAIN_DEPENDENCY ${SPHINX_SOURCE}/conf.py
|
||||
COMMENT "Generating documentation with Sphinx")
|
||||
|
||||
# Nice named target so we can run the job easily
|
||||
add_custom_target(Sphinx ALL DEPENDS ${SPHINX_INDEX_FILE})
|
||||
11
Docs/Doxyfile.in
Normal file
11
Docs/Doxyfile.in
Normal file
@ -0,0 +1,11 @@
|
||||
// Every other setting ends up being default.
|
||||
|
||||
GENERATE_XML = YES
|
||||
GENERATE_HTML = YES
|
||||
|
||||
JAVADOC_AUTOBRIEF = YES
|
||||
|
||||
INPUT = @DOXYGEN_INPUT_DIRS@
|
||||
RECURSIVE = YES
|
||||
|
||||
OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIR@"
|
||||
31
Docs/conf.py
Normal file
31
Docs/conf.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'SkullC Peripherals Library'
|
||||
copyright = '2023, Rusted Skull'
|
||||
author = 'Rusted Skull'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [ "breathe" ]
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'classic'
|
||||
html_static_path = ['_static']
|
||||
|
||||
# -- Breathe Configuration"
|
||||
breathe_default_project = "SkullCPeripheralsLibrary"
|
||||
|
||||
23
Docs/index.rst
Normal file
23
Docs/index.rst
Normal file
@ -0,0 +1,23 @@
|
||||
.. SkullC Peripherals Library documentation master file, created by
|
||||
sphinx-quickstart on Thu Dec 28 23:16:21 2023.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to SkullC Peripherals Library's documentation!
|
||||
======================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
utility
|
||||
peripherals
|
||||
threads
|
||||
messaging
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
17
Docs/messaging.rst
Normal file
17
Docs/messaging.rst
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
Messaging Library
|
||||
=================
|
||||
|
||||
A small Packet and Parser implementation for framing random message structures on the wire.
|
||||
|
||||
Use the :cpp:struct:`Packet` to copy in various data. Then send the serialized version of the packet
|
||||
onto the wire.
|
||||
|
||||
:cpp:class:`Messaging::Parser` will help you put bytes coming in from the wire into coherent packets, and will serve you
|
||||
these packets as you're ready for them.
|
||||
|
||||
.. doxygennamespace:: Messaging
|
||||
:project: SkullCPeripheralsLibrary
|
||||
:content-only:
|
||||
:members:
|
||||
:undoc-members:
|
||||
9
Docs/peripherals.rst
Normal file
9
Docs/peripherals.rst
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
Peripherals Library
|
||||
=================
|
||||
|
||||
.. doxygennamespace:: Peripherals
|
||||
:project: SkullCPeripheralsLibrary
|
||||
:content-only:
|
||||
:members:
|
||||
:undoc-members:
|
||||
9
Docs/threads.rst
Normal file
9
Docs/threads.rst
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
Threads Library
|
||||
=================
|
||||
|
||||
.. doxygennamespace:: Threads
|
||||
:project: SkullCPeripheralsLibrary
|
||||
:content-only:
|
||||
:members:
|
||||
:undoc-members:
|
||||
9
Docs/utility.rst
Normal file
9
Docs/utility.rst
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
Utility Library
|
||||
=================
|
||||
|
||||
.. doxygennamespace:: Utility
|
||||
:project: SkullCPeripheralsLibrary
|
||||
:content-only:
|
||||
:members:
|
||||
:undoc-members:
|
||||
50
Jenkinsfile
vendored
50
Jenkinsfile
vendored
@ -1,50 +0,0 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'ubuntu'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage("build && test") {
|
||||
steps {
|
||||
echo "Workspace: ${env.WORKSPACE}"
|
||||
sh 'mkdir -p build'
|
||||
|
||||
dir("build") {
|
||||
sh 'ls'
|
||||
sh 'cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DSKULLC_WITH_TESTS=ON'
|
||||
sh 'ninja'
|
||||
sh 'ctest . -T test --output-on-failure --no-compress-output'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts (
|
||||
artifacts: 'build/Testing/**/*.xml',
|
||||
fingerprint: true
|
||||
)
|
||||
|
||||
// Process the CTest xml output with the xUnit plugin
|
||||
xunit (
|
||||
testTimeMargin: '3000',
|
||||
thresholdMode: 1,
|
||||
thresholds: [
|
||||
skipped(failureThreshold: '0'),
|
||||
failed(failureThreshold: '0')
|
||||
],
|
||||
tools: [CTest(
|
||||
pattern: 'build/Testing/**/*.xml',
|
||||
deleteOutputFiles: true,
|
||||
failIfNotNew: false,
|
||||
skipNoTestFiles: true,
|
||||
stopProcessingIfError: true
|
||||
)]
|
||||
)
|
||||
|
||||
// Clear the source and build dirs before next run
|
||||
deleteDir()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Peripherals/Inc/peripherals_hal_concepts.hpp
Normal file
37
Peripherals/Inc/peripherals_hal_concepts.hpp
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// Created by erki on 11/02/24.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Peripherals::Hal
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
concept Gpio = requires (T t, const T ct) {
|
||||
t.set(true);
|
||||
t.toggle();
|
||||
{ ct.read() } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
concept SerialInterface = requires (T t, std::uint8_t* data, const std::uint32_t len) {
|
||||
{ t.transmit(data, len) } -> std::same_as<bool>;
|
||||
{ t.transmit(std::array<std::uint8_t, 10>{}) } -> std::same_as<bool>;
|
||||
{ t.receive(data, len) } -> std::same_as<bool>;
|
||||
{ t.receive(std::array<std::uint8_t, 10>{}) } -> std::same_as<bool>;
|
||||
};
|
||||
|
||||
template<typename T, typename RegName>
|
||||
concept RegisterInterface = requires (T t, RegName reg, std::uint8_t* data, const std::uint32_t len) {
|
||||
t.writerRegister(reg, std::uint8_t{});
|
||||
t.writeRegisterMultibyte(reg, data, len);
|
||||
{ t.readRegister(reg, std::uint32_t{}) } -> std::same_as<std::uint8_t>;
|
||||
t.readRegisterMultibyte(reg, data, len, std::uint32_t{});
|
||||
};
|
||||
|
||||
}
|
||||
51
Peripherals/Inc/peripherals_hal_esp.hpp
Normal file
51
Peripherals/Inc/peripherals_hal_esp.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Created by erki on 11/02/24.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef SKULLC_USE_HAL_ESP
|
||||
|
||||
#include <peripherals_hal_concepts.hpp>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
namespace Peripherals::Hal::Esp
|
||||
{
|
||||
|
||||
struct Gpio
|
||||
{
|
||||
gpio_num_t gpio;
|
||||
const bool inverted = false;
|
||||
|
||||
Gpio() = delete;
|
||||
explicit Gpio(gpio_num_t gpio, const bool inverted)
|
||||
: gpio(gpio), inverted(inverted)
|
||||
{ }
|
||||
|
||||
void set(const bool& state)
|
||||
{
|
||||
gpio_set_level(gpio, inverted ? !state : state);
|
||||
}
|
||||
|
||||
void toggle() { gpio_set_level(gpio, !gpio_get_level(gpio)); }
|
||||
|
||||
bool read() const
|
||||
{
|
||||
return inverted ? !bool(gpio_get_level(gpio)) : bool(gpio_get_level(gpio));
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(Peripherals::Hal::Gpio<Gpio>);
|
||||
|
||||
#define CREATE_GPIO(name) \
|
||||
Peripherals::Hal::Esp::Gpio{name, false}
|
||||
|
||||
#define CREATE_INV_GPIO(name) \
|
||||
Peripherals::Hal::Esp::Gpio{name, true}
|
||||
|
||||
}
|
||||
|
||||
#else
|
||||
# warning "ESP HAL included without SKULLC_USE_HAL_ESP being defined."
|
||||
#endif /* SKULLC_USE_HAL_ESP */
|
||||
@ -8,6 +8,8 @@
|
||||
#ifndef SKULLC_PERIPHERALS_HAL_ST_HPP_
|
||||
#define SKULLC_PERIPHERALS_HAL_ST_HPP_
|
||||
|
||||
#ifdef SKULLC_USE_HAL_ST
|
||||
|
||||
#include <main.h>
|
||||
|
||||
#include <array>
|
||||
@ -343,4 +345,8 @@ struct ItmSerialInterface
|
||||
}// namespace Hal
|
||||
}// namespace Peripherals
|
||||
|
||||
#else
|
||||
# warning "ESP HAL included without SKULLC_USE_HAL_ESP being defined."
|
||||
#endif /* SKULLC_USE_HAL_ST */
|
||||
|
||||
#endif /* SKULLC_PERIPHERALS_HAL_ST_HPP_ */
|
||||
|
||||
@ -13,8 +13,6 @@
|
||||
|
||||
#include "peripherals_imu.hpp"
|
||||
|
||||
#include "utility_logging.hpp"
|
||||
|
||||
namespace Peripherals
|
||||
{
|
||||
|
||||
@ -45,7 +43,10 @@ public:
|
||||
|
||||
ImuIcm() = delete;
|
||||
ImuIcm(const registers_handle& registers) : registers(registers)
|
||||
{ }
|
||||
{
|
||||
bias_accel_.fill(0);
|
||||
bias_gyro_.fill(0);
|
||||
}
|
||||
|
||||
void setup() override
|
||||
{
|
||||
@ -70,9 +71,13 @@ public:
|
||||
// rate = 1KHz, temp filter = 42
|
||||
hal::delay(10);
|
||||
|
||||
setAccelerometerScale(scale_accel_);
|
||||
const std::uint8_t new_gyro_conf = (std::uint32_t(scale_gyro_) << 3);
|
||||
registers.writeRegister(Registers_::GYRO_CONFIG & Registers_::WRITE_MASK,
|
||||
new_gyro_conf);
|
||||
|
||||
setGyroscopeScale(scale_gyro_);
|
||||
const std::uint8_t new_accel_config = (std::uint32_t(scale_accel_) << 3);
|
||||
registers.writeRegister(Registers_::ACCEL_CONFIG & Registers_::WRITE_MASK,
|
||||
new_accel_config);
|
||||
|
||||
// ACCEL_FCHOICE_B = 0, A_DLPF_CFG = 3 filter=44.8/61.5 rate=1KHz
|
||||
registers.writeRegister(Registers_::ACCEL_CONFIG2 & Registers_::WRITE_MASK,
|
||||
@ -94,25 +99,27 @@ public:
|
||||
0);
|
||||
hal::delay(10);
|
||||
|
||||
registers.writeRegister(Registers_::INT_ENABLE & Registers_::WRITE_MASK, 0);
|
||||
registers.writeRegister(Registers_::INT_ENABLE & Registers_::WRITE_MASK, 1);
|
||||
hal::delay(10);
|
||||
}
|
||||
|
||||
std::array<std::int16_t, 3>& gyroBias()
|
||||
{
|
||||
return bias_gyro_;
|
||||
}
|
||||
|
||||
std::array<std::int16_t, 3>& accelBias()
|
||||
{
|
||||
return bias_accel_;
|
||||
}
|
||||
|
||||
void calibrate(const std::uint32_t samples) override
|
||||
{
|
||||
const auto prev_accel_scale = scale_accel_;
|
||||
const auto prev_gyro_scale = scale_gyro_;
|
||||
std::array<std::int32_t, 3> avg_gyro;
|
||||
std::array<std::int32_t, 3> avg_accel;
|
||||
|
||||
setAccelerometerScale(AccelerometerScale::G2);
|
||||
setGyroscopeScale(GyroScale::DPS_250);
|
||||
|
||||
std::array<std::int32_t, 3> avg_gyro = {0, 0, 0};
|
||||
std::array<std::int32_t, 3> avg_accel = {0, 0, 0};
|
||||
std::array<std::int32_t, 3> bias_gyro = {0, 0, 0};
|
||||
std::array<std::int32_t, 3> bias_accel = {0, 0, 0};
|
||||
|
||||
[[maybe_unused]] const std::uint16_t gyro_sensitivity = 131; // = 131 LSB/degrees/sec
|
||||
const std::uint16_t accel_sensitivity = 16384; // = 16384 LSB/g
|
||||
avg_gyro.fill(0);
|
||||
avg_accel.fill(0);
|
||||
|
||||
for (std::uint32_t i = 0; i < samples; i++)
|
||||
{
|
||||
@ -131,65 +138,11 @@ public:
|
||||
|
||||
for (std::uint32_t i = 0; i < 3; i++)
|
||||
{
|
||||
bias_gyro[i] = avg_gyro[i] / std::int32_t(samples);
|
||||
bias_accel[i] = avg_accel[i] / std::int32_t(samples);
|
||||
bias_gyro_[i] = avg_gyro[i] / std::int32_t(samples);
|
||||
bias_accel_[i] = avg_accel[i] / std::int32_t(samples);
|
||||
}
|
||||
|
||||
if (bias_accel[2] > 0)
|
||||
bias_accel[2] -= accel_sensitivity;
|
||||
else
|
||||
bias_accel[2] += accel_sensitivity;
|
||||
|
||||
std::array<std::uint8_t, 6> push_data;
|
||||
|
||||
// Construct the gyro biases for push to the hardware gyro bias registers,
|
||||
// which are reset to zero upon device startup.
|
||||
// Divide by 4 to get 32.9 LSB per deg/s to conform to expected bias input
|
||||
// format.
|
||||
push_data[0] = (-bias_gyro[0]/4 >> 8) & 0xFF;
|
||||
push_data[1] = (-bias_gyro[0]/4) & 0xFF;
|
||||
push_data[2] = (-bias_gyro[1]/4 >> 8) & 0xFF;
|
||||
push_data[3] = (-bias_gyro[1]/4) & 0xFF;
|
||||
push_data[4] = (-bias_gyro[2]/4 >> 8) & 0xFF;
|
||||
push_data[5] = (-bias_gyro[2]/4) & 0xFF;
|
||||
|
||||
registers.writeRegisterMultibyte(Registers_::XG_OFFSET_H & Registers_::WRITE_MASK,
|
||||
push_data.data(), push_data.size());
|
||||
|
||||
std::array<std::int32_t, 3> factory_values = {0, 0, 0};
|
||||
// registers.readRegisterMultibyte(Registers_::XA_OFFSET_H | Registers_::READ_MASK,
|
||||
// push_data.data(), push_data.size());
|
||||
// factory_values[0] = std::int32_t((std::int16_t(push_data[0]) << 8) | push_data[1]);
|
||||
// factory_values[1] = std::int32_t((std::int16_t(push_data[2]) << 8) | push_data[3]);
|
||||
// factory_values[2] = std::int32_t((std::int16_t(push_data[4]) << 8) | push_data[5]);
|
||||
|
||||
// Construct total accelerometer bias, including calculated average
|
||||
// accelerometer bias from above
|
||||
// Subtract calculated averaged accelerometer bias scaled to 2048 LSB/g
|
||||
// (16 g full scale)
|
||||
bias_accel[0] = factory_values[0] - (bias_accel[0] / 8);
|
||||
bias_accel[1] = factory_values[1] - (bias_accel[1] / 8);
|
||||
bias_accel[1] = factory_values[1] - (bias_accel[1] / 8);
|
||||
|
||||
push_data[0] = ((bias_accel[0] >> 8) & 0xFF);
|
||||
push_data[1] = ((bias_accel[0]) & 0xFF) | (factory_values[0] & 0x1);
|
||||
push_data[2] = ((bias_accel[1] >> 8) & 0xFF);
|
||||
push_data[3] = ((bias_accel[1]) & 0xFF) | (factory_values[1] & 0x1);
|
||||
push_data[4] = ((bias_accel[2] >> 8) & 0xFF);
|
||||
push_data[5] = ((bias_accel[2]) & 0xFF) | (factory_values[2] & 0x1);
|
||||
|
||||
registers.writeRegisterMultibyte(Registers_::XA_OFFSET_H & Registers_::WRITE_MASK,
|
||||
push_data.data(), push_data.size());
|
||||
|
||||
// __asm__("BKPT");
|
||||
|
||||
setAccelerometerScale(prev_accel_scale);
|
||||
setGyroscopeScale(prev_gyro_scale);
|
||||
|
||||
SKULLC_LOG_DEBUG("ICM: Accelerometer bias: %d, %d, %d; accel total: %d, %d, %d.",
|
||||
bias_accel[0], bias_accel[1], bias_accel[2],
|
||||
avg_accel[0], avg_accel[1], avg_accel[2]);
|
||||
// TODO: remove manual bias calculations. These are now done in hardware.
|
||||
bias_accel_[2] -= accelerometerReadingToRaw(1);
|
||||
}
|
||||
|
||||
void setGyroscopeScale(const GyroScale scale)
|
||||
@ -273,12 +226,12 @@ public:
|
||||
|
||||
float accelerometerRawToReading(const std::int16_t bit, const std::size_t axis) const
|
||||
{
|
||||
return (float(bit) * accel_fs_to_bit_constants_[std::uint32_t(scale_accel_)]) * 9.8f;
|
||||
return (float(bit - bias_accel_[axis]) * accel_fs_to_bit_constants_[std::uint32_t(scale_accel_)]) * 9.8f;
|
||||
}
|
||||
|
||||
float gyroRawToReading(const std::int16_t bit, const std::size_t axis) const
|
||||
{
|
||||
return float(bit) * gyro_fs_to_bit_constants_[std::uint32_t(scale_gyro_)];
|
||||
return float(bit - bias_gyro_[axis]) * gyro_fs_to_bit_constants_[std::uint32_t(scale_gyro_)];
|
||||
}
|
||||
|
||||
std::uint8_t readProductId()
|
||||
@ -298,6 +251,9 @@ private:
|
||||
(250.0f / 32768.0f), (500.0f / 32768.0f), (1000.0f / 32768.0f),
|
||||
(2000.0f / 32768.0f)};
|
||||
|
||||
std::array<std::int16_t, 3> bias_gyro_;
|
||||
std::array<std::int16_t, 3> bias_accel_;
|
||||
|
||||
struct Registers_
|
||||
{
|
||||
static constexpr std::uint32_t ICM20689_ID = 0x98;
|
||||
|
||||
@ -30,6 +30,7 @@ add_executable(tests
|
||||
assert.cpp
|
||||
enum_helpers.cpp
|
||||
bytes.cpp
|
||||
filters.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(tests
|
||||
|
||||
191
Tests/filters.cpp
Normal file
191
Tests/filters.cpp
Normal file
@ -0,0 +1,191 @@
|
||||
//
|
||||
// Created by erki on 12/11/22.
|
||||
//
|
||||
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include "utility_filters.hpp"
|
||||
|
||||
TEST_CASE("LowPassFilter works as expected.")
|
||||
{
|
||||
const int threshold = 50;
|
||||
Utility::LowPassFilter<int> filter{threshold};
|
||||
|
||||
SECTION("Initial value is 0.")
|
||||
{
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
}
|
||||
|
||||
SECTION("Passing a number higher than threshold gets filtered.")
|
||||
{
|
||||
filter.update(60);
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
|
||||
SECTION("Filter updates state as expected.")
|
||||
{
|
||||
filter.update(40);
|
||||
REQUIRE(filter.currentValue() == 40);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Passing a number lower than threshold passes filter.")
|
||||
{
|
||||
filter.update(40);
|
||||
REQUIRE(filter.currentValue() == 40);
|
||||
|
||||
SECTION("Filter retains state as expected.")
|
||||
{
|
||||
filter.update(60);
|
||||
REQUIRE(filter.currentValue() == 40);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("HighPassFilter works as expected.")
|
||||
{
|
||||
const int threshold = 50;
|
||||
Utility::HighPassFilter<int> filter{threshold};
|
||||
|
||||
SECTION("Initial value is 0.")
|
||||
{
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
}
|
||||
|
||||
SECTION("Passing a number higher than threshold passes filter.")
|
||||
{
|
||||
filter.update(60);
|
||||
REQUIRE(filter.currentValue() == 60);
|
||||
|
||||
SECTION("Filter retains state as expected.")
|
||||
{
|
||||
filter.update(40);
|
||||
REQUIRE(filter.currentValue() == 60);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Passing a number lower than threshold gets filtered.")
|
||||
{
|
||||
filter.update(40);
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
|
||||
SECTION("Filter updates state as expected.")
|
||||
{
|
||||
filter.update(60);
|
||||
REQUIRE(filter.currentValue() == 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Two filters can be linked together.")
|
||||
{
|
||||
const int threshold_low = 40;
|
||||
const int threshold_high = 50;
|
||||
|
||||
Utility::LowPassFilter<int> low_pass{threshold_high};
|
||||
Utility::HighPassFilter<int> high_pass{threshold_low};
|
||||
|
||||
high_pass.assignPrecedingFilter(low_pass);
|
||||
|
||||
SECTION("Initial value is 0.")
|
||||
{
|
||||
REQUIRE(high_pass.currentValue() == 0);
|
||||
}
|
||||
|
||||
SECTION("Values outside of band are filtered out.")
|
||||
{
|
||||
high_pass.update(30);
|
||||
REQUIRE(high_pass.currentValue() == 0);
|
||||
|
||||
high_pass.update(60);
|
||||
REQUIRE(high_pass.currentValue() == 0);
|
||||
|
||||
SECTION("Filter updates with in-band value.")
|
||||
{
|
||||
high_pass.update(45);
|
||||
REQUIRE(high_pass.currentValue() == 45);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Values inside of band are passed through.")
|
||||
{
|
||||
high_pass.update(44);
|
||||
REQUIRE(high_pass.currentValue() == 44);
|
||||
|
||||
high_pass.update(46);
|
||||
REQUIRE(high_pass.currentValue() == 46);
|
||||
|
||||
SECTION("Out of band values are filtered out.")
|
||||
{
|
||||
high_pass.update(30);
|
||||
REQUIRE(high_pass.currentValue() == 46);
|
||||
|
||||
high_pass.update(60);
|
||||
REQUIRE(high_pass.currentValue() == 46);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Decoupling filters works as expected.")
|
||||
{
|
||||
high_pass.update(60);
|
||||
REQUIRE(high_pass.currentValue() == 0);
|
||||
|
||||
high_pass.clearPrecedingFilter();
|
||||
high_pass.update(60);
|
||||
REQUIRE(high_pass.currentValue() == 60);
|
||||
|
||||
high_pass.update(30);
|
||||
REQUIRE(high_pass.currentValue() == 60);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Median filter works as expected.")
|
||||
{
|
||||
Utility::MedianFilter<int, 5> filter;
|
||||
|
||||
SECTION("Initial value is 0.")
|
||||
{
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
}
|
||||
|
||||
SECTION("The median is filtered appropriately.")
|
||||
{
|
||||
std::array<int, 5> data = {5, 1, 4, 3, 2};
|
||||
std::array<int, 5> expected_median = {0, 0, 1, 3, 3};
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
const int x = data[i];
|
||||
filter.update(x);
|
||||
REQUIRE(filter.currentValue() == expected_median[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Rolling average filter works as expected.")
|
||||
{
|
||||
Utility::RollingAverageFilter<int> filter{5};
|
||||
|
||||
SECTION("Initial value is 0.")
|
||||
{
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
}
|
||||
|
||||
SECTION("Value below step size is filtered out for int filter.")
|
||||
{
|
||||
filter.update(4);
|
||||
REQUIRE(filter.currentValue() == 0);
|
||||
}
|
||||
|
||||
SECTION("Value is updated appropriately.")
|
||||
{
|
||||
filter.update(20);
|
||||
REQUIRE(filter.currentValue() == 20 / 5);
|
||||
|
||||
SECTION("Saturation is reached appropriately")
|
||||
{
|
||||
for (int i = 0; i < 5 * 2 - 1; i++)
|
||||
filter.update(20);
|
||||
|
||||
REQUIRE(filter.currentValue() == 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Utility/Inc/utility_filters.hpp
Normal file
124
Utility/Inc/utility_filters.hpp
Normal file
@ -0,0 +1,124 @@
|
||||
//
|
||||
// Created by erki on 12/11/22.
|
||||
//
|
||||
|
||||
#ifndef SKULLC_UTILITY_FILTERS_HPP_
|
||||
#define SKULLC_UTILITY_FILTERS_HPP_
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include "utility_ifilter.hpp"
|
||||
|
||||
namespace Utility
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
class RollingAverageFilter : public IFilter<T>
|
||||
{
|
||||
public:
|
||||
RollingAverageFilter() = delete;
|
||||
explicit RollingAverageFilter(const std::size_t step)
|
||||
: step_size_(step)
|
||||
{}
|
||||
|
||||
T currentValue() const override
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
protected:
|
||||
void updateImpl_(const T& data) override
|
||||
{
|
||||
value_ -= value_ / step_size_;
|
||||
value_ += data / step_size_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t step_size_;
|
||||
T value_ = 0;
|
||||
};
|
||||
|
||||
template<typename T, std::size_t N>
|
||||
class MedianFilter : public IFilter<T>
|
||||
{
|
||||
public:
|
||||
MedianFilter()
|
||||
{
|
||||
data_sequence_.fill(T(0));
|
||||
}
|
||||
|
||||
T currentValue() const override
|
||||
{
|
||||
auto sorted = data_sequence_;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
|
||||
return sorted[N / 2];
|
||||
}
|
||||
|
||||
protected:
|
||||
void updateImpl_(const T& data) override
|
||||
{
|
||||
std::move(data_sequence_.begin() + 1, data_sequence_.end(), data_sequence_.begin());
|
||||
data_sequence_[N - 1] = data;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<T, N> data_sequence_;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class LowPassFilter : public IFilter<T>
|
||||
{
|
||||
public:
|
||||
LowPassFilter() = delete;
|
||||
explicit LowPassFilter(const T threshold)
|
||||
: threshold_(threshold)
|
||||
{}
|
||||
|
||||
T currentValue() const override
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
protected:
|
||||
void updateImpl_(const T& data) override
|
||||
{
|
||||
if (data < threshold_)
|
||||
value_ = data;
|
||||
}
|
||||
|
||||
private:
|
||||
T threshold_;
|
||||
T value_ = 0;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class HighPassFilter : public IFilter<T>
|
||||
{
|
||||
public:
|
||||
HighPassFilter() = delete;
|
||||
explicit HighPassFilter(const T threshold)
|
||||
: threshold_(threshold)
|
||||
{}
|
||||
|
||||
T currentValue() const override
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
protected:
|
||||
void updateImpl_(const T& data) override
|
||||
{
|
||||
if (data > threshold_)
|
||||
value_ = data;
|
||||
}
|
||||
|
||||
private:
|
||||
T threshold_;
|
||||
T value_ = 0;
|
||||
};
|
||||
|
||||
}// namespace Utility
|
||||
|
||||
#endif//SKULLC_UTILITY_FILTERS_HPP_
|
||||
49
Utility/Inc/utility_ifilter.hpp
Normal file
49
Utility/Inc/utility_ifilter.hpp
Normal file
@ -0,0 +1,49 @@
|
||||
//
|
||||
// Created by erki on 12/11/22.
|
||||
//
|
||||
|
||||
#ifndef SKULLC_UTILITY_IFILTER_HPP_
|
||||
#define SKULLC_UTILITY_IFILTER_HPP_
|
||||
|
||||
namespace Utility
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
class IFilter
|
||||
{
|
||||
public:
|
||||
virtual ~IFilter() = default;
|
||||
|
||||
void assignPrecedingFilter(IFilter<T>& filter)
|
||||
{
|
||||
preceding_filter_ = &filter;
|
||||
}
|
||||
|
||||
void clearPrecedingFilter()
|
||||
{
|
||||
preceding_filter_ = nullptr;
|
||||
}
|
||||
|
||||
void update(T data)
|
||||
{
|
||||
if (preceding_filter_)
|
||||
{
|
||||
preceding_filter_->update(data);
|
||||
data = preceding_filter_->currentValue();
|
||||
}
|
||||
|
||||
updateImpl_(data);
|
||||
}
|
||||
|
||||
virtual T currentValue() const = 0;
|
||||
|
||||
protected:
|
||||
virtual void updateImpl_(const T& data) = 0;
|
||||
|
||||
private:
|
||||
IFilter<T>* preceding_filter_ = nullptr;
|
||||
};
|
||||
|
||||
}// namespace Utility
|
||||
|
||||
#endif//SKULLC_UTILITY_IFILTER_HPP_
|
||||
11
cmake/FindSphinx.cmake
Normal file
11
cmake/FindSphinx.cmake
Normal file
@ -0,0 +1,11 @@
|
||||
#Look for an executable called sphinx-build
|
||||
find_program(SPHINX_EXECUTABLE
|
||||
NAMES sphinx-build
|
||||
DOC "Path to sphinx-build executable")
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
#Handle standard arguments to find_package like REQUIRED and QUIET
|
||||
find_package_handle_standard_args(Sphinx
|
||||
"Failed to find sphinx-build executable"
|
||||
SPHINX_EXECUTABLE)
|
||||
@ -1,5 +0,0 @@
|
||||
[requires]
|
||||
catch2/[>=2.0.0,<3.0.0]
|
||||
|
||||
[generators]
|
||||
cmake_find_package
|
||||
Loading…
x
Reference in New Issue
Block a user