Developed and maintained by Vojtěch Michal (Discord vojtechmichal)
since season 2020/2021 at eForce (FEE) Prague Formula.
Universal collection of mostly header-only libraries containing stuff generally useful across various firmware projects.
Implemented features are separated into individual modules:
- Assert ... Implementation of macros
assert()andassert_unreachable() - BitOperations ... Bit operations, integer slicing, bit iteration
- Coroutine ... Various types for coroutine support missing in the standard library (as of C++20) - ability to wait on predicates or for fixed duration, wrappers for tasks and generators.
- DummySyscalls ... Minimal implementation of some syscall related functions to silence linker warnings.
- Functional ... Higher order functions and types enabling more precise control over the flow of control. Also algorithms for tuples.
- Itertools ... Collection of helpers bringing Python-like syntax to C++ iteration
- Logic ... OPerations on digital signals - edge detection, deglitch filters, comparators with hysteresis, etc.
- Math ... Various mathematical functions, digital filters, machinery for filtering raw ADC readings, etc.
- Memory ... Logic for advanced memory manipulation (static memory allocator for standard containers)
- QueryRepositoryState ... Stores SHA1 of compiled git commit and allows you to query state of the repository at runtime
- Time ... High level (and low resolution) timing functionality -
Timestamp,SysTickTimer, etc. - Traits ... Generally useful types and concepts, queries of properties of types etc.
- Units ... Collection of physical units. Defines literals, conversion operators and arithmetic.
Requirements:
- CMake ... tested on CMake 3.14
- Git ... expected to be used as git submodule
- Compilation requires language features of C++20
The library is separated into smaller modules (not in the sense of C++ language modules), that are mostly independent of each other. Each module has exactly one cmake file
situated in ufsel/cmake directory, that must be included by your project's CMakeLists.txt in order to make use of functionality of the corresponding module.
This cmake file automatically takes care of adding defines, compiler flags etc. to make requested module available.
The directory ufsel/include contains the C++ source code. Using a module (by means of including the corresponding cmake file)
automatically adds this whole directory to the include path. All header files contain preprocessor guards that disable all functionality
by default. The functionality is enabled only when corresponding cmake script is included.
Including a header file and using its content without including corresponsing cmake file will lead to compile time error.
Identifiers with the prefix UFSEL are reserved to implementation, PLEASE DO NOT DEFINE THOSE, UNLESS INSTRUCTED TO DO SO - several customization points are implemented using macros.
- Add this repository as submodule to the root of your firmware. Unless you have very specific needs, place the submodule directory into the repository root. To ensure the submodule is found by CI, either manually edit
.gitmodulesto use relative path or specifyGIT_SUBMODULE_FORCE_HTTPS: "true"in your.gitlab-ci.yml(see Gitlab Docs)
$ cd ecu-sw # The root of firmware repository where CMakeLists.txt is located
$ git submodule add ssh://[email protected]:2020/vomi/ufsel.git
# If needed, manually edit .gitmodules and change path to relative like this:
# [submodule "ufsel"]
# path = ufsel
# url = ../../vomi/ufsel.gitCHECKPOINT: You may commit and push the repository. If your CI pipeline does not fail when cloning the submodule, you have done it correctly.
- To use a specific module
Xof UFSEL library to its full potential, include the cmake script corresponding toXin yourCMakeLists.txt. Add this line (or similar depending on your directory structure) to yourCMakeLists.txtfile:
include("${CMAKE_CURRENT_LIST_DIR}/ufsel/cmake/FeatureYouAreInterestedIn.cmake")List of features you may be interested in follows. You can also check the directory ufsel/cmake to see precise file names.
Some modules require configuration by the user.
This configuration is implemented using dedicated configuration file (located in your project's root), whose name is passed to the library using
a preprocessor definition UFSEL_CONFIGURATION_FILE. Each module with some special requirements will state its expectations clearly in the header file
so refer to individual header files to learn more. Customization points are implemented as type aliases or constants
wrapped by a namespace corresponding to given module.
For example the BitOperations module expects your configuration file to define several types and values wrapped by
namespace ::ufsel::bit. These types inform the library - among others - about the size of target machine native word. The configuration may be done as follows:
# file toolchain.cmake
SET(CONFIG_FILES "-DUFSEL_CONFIGURATION_FILE=\"<ufsel-configuration.hpp>\"")// File ufsel-configuration.hpp located in the application root
#pragma once
#include <cstdint>
namespace diagnostics {
[[noreturn]] void AssertionFailed_Handler();
}
namespace ufsel {
namespace time {
//Initial value of the SystemTicks variable which stores value for Timestamp:now()
constexpr std::uint32_t systemStartTick = 0xcafe'babe;
}
namespace bit {
using machine_word = std::uint32_t;
}
namespace math {
using readout_reference_t = std::uint32_t volatile&;
}
namespace assertion {
/*Turns on and off assert functionality.
If false, all calls to assert() compile into a noop and thus have no influence on the program
If true, family of assert functions validate their argument and should an error be detected, SW breakpoint is hit. */
constexpr bool enableAssert = true;
/*If set to true, debugging experience is enhanced by breaking in the body of assert_fun when the tested condition fails.
False is the safer option, because failed assertion will lead straight to HF handler and hence safe state will be guaranteed.*/
constexpr bool breakInFailedAssert = false;
[[noreturn]] void assertionFailedHandler(char const * file, char const * function, int line);
[[noreturn]] void unreachableCodeHandler(char const * file, char const * function, int line);
}
}
All modules are thoroughly documented in their source code. This section contains additional overviews and summaries, but it is not a substitute for skimming through the module's source code.
Custom implementation of assert functionality that delegates to provided handler code (typically to diagnostics::EverythingsFuckedUp). It distinguishes a standard assert(condition) and assert_unreachable() used to mark never-to-be reached branches of code.
Macros UFSEL_USING_ASSERT and UFSEL_USING_CHEAP_ASSERT select whether the underlying handler function receives the file and function names or not. Providing them is generally preferred for ECUs
with standard output implementation, but it adds a lot of static strings to the binary. For applications with size restrictions, use the cheap assert.
Header only library (#include <ufsel/bit.hpp>) implementing
- common bitwise operations using variadic function templates,
- reading and mutation of subsequences of underlying bits of integral values,
- highly configurable iteration over bits of an integral value.
All functionality is located within namespace ufsel::bit.
Functions defined in this module operate on powers of two (or more generally bitmasks) instead of bit indices.
To acquire power of two with bit x high, use the function bit(x). e.g. the following holds: static_assert(bit(11) == 2048);.
The least significant bit has index zero.
Most of the functions have two overloads. One is a pure function taking only values of its arguments and returning the result of computation.
The other takes std::reference_wrapper as the first argument and performs modification in place (result is immediately written back to memory).
The reference_wrapper overload is only a syntactic sugar as can be seen from the next example. The following two expressions have identical behavior:
reg = ufsel::bit::clear(reg, bit(7), 0x1'00);`
ufsel::bit::clear(std::ref(reg), 128, 256);` // same resultFunctions in this module can be classified as follows:
- Bitmask generation functions:
bit,bitmask,bitmask_of_width,bitmask_between - Raw memory access:
access_register - Bitwise operations:
get,set,clear,toggle - Bitfield modification and querying:
modify,all_set,all_cleared - Blocking delay until some bitmask is set or cleared:
wait_until_set,wait_until_cleared
Please referer to ufsel/bit_operations.hpp for documentation of individual functions.
Objects of type sliceable wrap an integral value and expose operator[] returning a proxy object.
Classes bit_proxy and mutable_bit_proxy facilitate read access to contiguous subsequences of bits of the stored integral value.
Additionally, objects of type mutable_bit_proxy are assignable.
Reads and writes (assignments) though these objects perform bit shifting automatically, the user should therefore use them as if all bitfields
spanned bits [width-1 : 0].
If a written value is wider than the available number of bits, it is truncated modulo 2^width.
Integral value can be wrapped by sliceable either by reference or by copy. In former case all reads and writes access the referenced
memory location. In latter case, the copy of memory location is read and stored in an internal member variable, that is subsequently accessed.
Choice between value and reference semantics is performed based on a non-type template parameter of type sliceable,
or - more conveniently - using specialized types sliceable_value and sliceable_reference.
There exists a third variant of sliceable - the RAII type sliceable_with_deffered_writeback. This type uses the same value semantics
as sliceable_value, it howver keeps reference to the memory location and will copy the internal variable to referenced location
during destructor call. This type is especially useful when modifying fields of a register one by one, because it buffers modifications
internally (hence is eligible to be placed in CPU register) and writes to memory only when object lifetime ends.
There are two overloads of sliceable::operator[]. One accepts plain int, in which
case the function returns proxy to a single bit with specified index.
The other expects an object of type slice, whose constructor takes two indices - the higher
and the lower end of desired bitfield (both indices are inclusive).
Syntactic sugar in the form of user-defined literal operator and overloaded comma operator on sliceable
allow the user to use syntax shown in the next example when constructing an object of type slice.
Example: The statement
//'15_to, 8' is the same as 'sliceable{15, 8}'
sliceable_value{reg}[15_to, 8] = 0x123`;writes 0x23 to byte 1 of reg. No other bits are modified.
Since the reg has been wrapped by value, this write access does not propagate
to memory and hence is discarded a the end of this full expression (because of the end of temporary's lifetime).
If sliceable_reference was used, then the modification would take effect immediatelly.
If sliceable_with_deffered_writeback was used, then the modification would wait till object destructor.
In this case the object would be a temporary, hence the writeback would occur at the end of full expression.
TODO to be filled
Library support for C++20 coroutines, originally missing in the standard. TODO to be filled
TODO to be filled
TODO to be filled
TODO to be filled
Simple common wrappers around logic values (e.g. edge detectors), threshold comparators and deglitch filters.
Various mathematical functions and utilities, e.g. part of motor controller math library originally developed for Disruptor. Contains low-pass filters, wrappers for converted and filtered ADC readings, function align_up etc.
Provides StaticAllocator - a stateful allocator for standard containers (though it is tested only with std::vector) containing all elements in-place in its own storage.
Cornerstone of SoftwareBuild messages sent to CAN. Modifies the build process to issue a call to git rev-parse during compilation in order to retrieve the SHA1 hash of HEAD.
Eight leading characters of the hash are then passed to the compiler as -DUFSEL_GIT_COMMIT_HASH=wwxxyyzz.
Additionally, this feature detects whether the currently compiled git repository has dirty working tree with uncommitted changes.
The macro UFSEL_GIT_REPO_DIRTY is defined to either one or zero accordingly and may be used by client code.
Using this cmake module in your project activates the content of header ufsel/sw_build.hpp, which contains API to query
the current repository state.
//Synopsis of file include/ufsel/sw_build.hpp
namespace ufsel::git {
std::uint32_t commit_hash();
bool has_dirty_working_tree();
}Standardized implementation of high-level timing in our firmwares. It is based on ufsel::units::Duration with millisecond resolution.
The struct Timestamp represents a unique timestamp in milliseconds queried by the static member-function Timestamp::Now() that must be provided by the user.
Objects of type SysTickTimer provide an API for periodic execution of tasks using the if (timer.RestartIfTimeElapsed(period)) { /**/ } pattern.
Various metaprogramming features inspired by standard header <type_traits> required by other UFSEL modules (such as Functional) and other libraries, e.g. CANEPP. Also includes types required to support Enum objects in CANEPP.
Strongly typed implementation of physical units (Voltage, Current, Temperature, Duration etc.) provided in namespace ufsel::units. Internally uses ints rather than floats to improve execution speed on chips without FPU. This may result in large quantization errors and possible overflows/underflows of intermediate results in arithmetic expressions.
There are no operators defined between different units, so Voltage v = Resistance{r} * Current{i}; does not compile. This is by design since various units have various underlying representations and such operations would be hard to implement with the current design. SHould such calculations be needed (e.g. in ADC reading conversion), convert all strongly-typed objects to ints/floats and run calculation with them.
All units have associated literals (15_V, 10_A, 60_degC, 300_ms, etc.) defined in nested namespace ufsel::units::literals. Defining macros UFSEL_USING_UNIT_LITERALS and UFSEL_USING_UNITS will insert a declaration using namespace ufsel::units or ufsel::units::literals, respectively, into the global namespace.