ποΈ WARNING: PROJECT IN DEVELOPMENT π§
| Install | Overview | Glossary | Usage | Library Badges | Development Guides | Libraries | Motivation | Contributing |
Install from conan:
conan install libembeddedhal
Installing from source locally:
git clone https://github.com/libembeddedhal/libembeddedhal.git
cd libembeddedhal
conan create .
libembeddedhal exists to make hardware drivers:
- π portable
- π¦Ύ flexible
- π¦ accessible
- π° easy to use
- Header only
- Available on Conan (coming soon to vcpkg)
- Does not throw exceptions
- Does not dynamically allocate
- Uses tweak header files for customization
- Designed to be modular, dynamic, composable, and lightweight
- Dependencies:
- C++20 or above
- Boost.LEAF for error handling
- libxbitset
- mp-units
- uintwide_t.h
- System agnostic
- Follows C++ Core Guidelines
Here is a list of terms used in libembeddedhal. It is HIGHLY RECOMMENDED that new users of libembeddedhal read this section.
Targets are defined as MCUs (micro-controllers), SOCs (system-on-chip), operating systems, or operating systems running on a particular SBC (single-board-computer).
- LPC40xx series family of MCUs
- STM32F10x series family of MCUs
- RP2040
- AM335x
- Samsung Exynos5422
- Linux
- Windows CE
- Raspberry Pi
- ODROID UX
- BeagleBone Black
Interfaces are the basic building block of libembeddedhal and enables the flexibility needed to be portable and flexible.
An interface is a contract of functions that an implementing class must adhere to. Interface documentation explains in detail the expected behavior that each function should have on hardware regardless of the implementation. When a program is compiled and a driver implements an interface, the compiler detects if any of the functions have not been provided and if so, will report an error.
In libembeddedhal each interface corresponds to a type of embedded systems primitive which can be things such as:
- Digital I/O (input/output pins)
- Analog to digital converter (adc)
- Pulse width modulation (pwm)
- Serial peripheral interface (spi)
- Universal asynchronous receiver transmitter (serial/uart)
- Accelerometer
Peripheral drivers are drivers for a target that is embedded within the device and therefore cannot be removed from the chip and is fixed in number.
- Example: A digital output and input pin
- Example: 1 of 5 hardware timer within a micro-controller
- Example: Integrated analog-to-digital converter
Are drivers for devices external to a target. In order to communicate with such a device the target must have the necessary peripherals and peripheral drivers to operate correctly.
- Example: an accelerometer driver for the mpu6050
- Example: a memory storage driver for a at581 flash memory
- Example: a black and white pixel display
Are drivers that do not have any specific underlying hardware associated with them. They are used to emulate, give context to, or alter the behavior of interfaces. For a driver to be a soft driver it must implement or have a way to generate, construct or create implementations of hardware interfaces.
- Emulate spi by using 2 output pins and 1 input pin.
- Emulate uart transmission with a 16-bit spi driver and some clever bit positioning.
- Implement a rotary encoder by using an adc a potentiometer and some specification of the potentiometer like min and max angle, along with min and max voltage.
- Implement a dac using multiple output pins and a set of resistors and an op amp.
- Implement an input pin that inverts the readings of an actual input pin
- Implement an i2c driver that is thread safe by taking an i2c and locking mechanism provided by the user.
In general, software drivers tend to incur some overhead so nesting them deeply will effect performance.
Hard drivers are peripheral drivers and device drivers.
Off interface functions are public class functions that a driver can have that is beyond what is available for the interface it is implementing. These functions usually configures a peripheral or device in a way that is outside of the scope of the implementing interface. For peripherals these are platform specific. For drivers these are device specific features. Examples of such specific functions are as follows:
- An output pin driver with a high drain current mode
- An input pin driver with support for inverting the voltage level of what it reads in hardware.
- Enabling/disabling continuous sampling from an accelerometer where sampling continuously would make reading samples faster but would consume more power and disabling continuous sampling would do the opposite.
This section will go over how to use libembeddedhal in general. For details pertaining to specific interfaces see the π Software APIs for more details.
Using an lpc4078:
// Include driver code
#include <liblpc40xx/output_pin.hpp>
#include <liblpc40xx/input_pin.hpp>
int main() {
// Get pin P0[2] as an output pin
embed::output_pin & led = embed::lpc40xx::output_pin::get<0, 2>();
// Get pin P1[6] as an input pin
embed::input_pin & button = embed::lpc40xx::input_pin::get<1, 6>();
while (true)
{
if (button.level().value() == true) {
led.level(true);
} else {
led.level(false);
}
}
}
NOTE: that normally you wouldn't just get the value out of the function
button.level()
using the ::value()
function because this exits the
application if the response contains an error. Luckily
embed::lpc40xx::input_pin
never returns an error so the error handling check
can be ignored.
Using stm32f10x:
#include <chrono>
#include <libarmcortex/counter.hpp>
#include <libembeddedhal/counter/util.hpp>
#include <libstm32f10x/output_pin.hpp>
#include <libarmcortex/dwt_counter.hpp>
int main() {
// Get pin A2 as an output pin
embed::output_pin & led = embed::stm32f10x::output_pin::get<'A', 2>();
// Construct a hardware counter
embed::counter & counter = embed::cortex_m::dwt_counter::get(
embed::stm32f10x::clock::cpu());
while (true)
{
using std::chrono::literals;
led.level(true);
embed::delay(counter, 500ms);
led.level(false);
embed::delay(counter, 500ms);
}
}
libembeddedhal attempts to keep the organization of source code simple and consistent in order to make including libraries easy to remember.
The file organization follows these rules:
- Only 2 layers deep, excluding the
internal/
directory. - Non-hardware related utilities are placed at the root of the directory.
- Each interface has a directory at the root of the libembeddedhal directory.
- Each interface directory will have an
interface.hpp
file.- Example:
#include <libembeddedhal/adc/interface.hpp>
- Example:
#include <libembeddedhal/dac/interface.hpp>
- Example:
- Any files associated/extending a particular interface will reside in that interfaces directory such as soft drivers or utilities.
- Any hardware/interface files that extend to multiple interfaces will be
placed in one interface directories. The choice should be the directory that
makes the most sense, but this can be very arbitrary.
- Example:
#include <libembeddedhal/input_pin/pin_resistor.hpp>
, this file could be inoutput_pin
orinterrupt_pin
butinput_pin
seems like the best choice but is effectively arbitrary.
- Example:
libembeddedhal/
βββ config.hpp
βββ <interface_1> (the name of an example interface)
β βββ interface.hpp (REQUIRED: the interface definition is found here)
β βββ utility_class.hpp (some interfaces have utility classes as well)
β βββ mock.hpp (mocks for unit testing can be found here)
β βββ unit.hpp (contains any units associated with the interface)
β βββ util.hpp (utilities for the interface can be found here)
βββ i2c (example interface)
β βββ interface.hpp (holds the embed::i2c interface)
β βββ thread_safe.hpp (holds a soft driver implementing embed::i2c but with lock support)
β βββ util.hpp (holds embed::i2c utilities)
βββ internal (internal code that should NOT be accessed directly)
β βββ third_party (dependencies for libembeddedhal)
β βββ leaf.hpp (add Boost.LEAF for error handling and transport)
β βββ uintwide_t.h (add support for integers above 64-bits in width)
β βββ units (add physical unit support)
βββ enum.hpp (utility to handle enumerations)
βββ error.hpp (error handling code is found here)
βββ frequency.hpp (definition of the frequency type is found here)
βββ math.hpp (helper math functions)
βββ overflow_counter.hpp (detecting counter overflow)
βββ percent.hpp (defines the percent class)
βββ static_callable.hpp (convert polymorphic functions into free functions, useful for ISRs)
βββ static_memory_resource.hpp (static buffer for pmr containers)
βββ testing.hpp (utilities for unit testing)
βββ to_array.hpp (contains conversions from containers to std::array)
Utility functions help eliminate boilerplate code for the application and driver
writers. They provide common semantics for drivers such as being able to
call embed::read(/* insert interface here */)
on interfaces that have
read/sample capabilities.
Meaning that the following code should work for all three of these functions.
constexpr std::byte address(0x17);
auto response_i2c = embed::read<1>(i2c, address);
auto response_spi = embed::read<1>(spi);
auto response_uart = embed::read<1>(uart);
Utility functions are always "free" functions. "Free" means a non-class member function. Utility functions should never need access to the internal details of a class and thus do not and should not be members of the class.
When C++ adds support for UFCS (Uniform function call syntax) free functions can be called as if they were member functions and class functions could be called as if they were free functions. Slated currently for C++26.
An example of UFCS would be the following:
// These two will be equivalent in C++26
auto c_style_call = embed::read<1>(spi);
auto ufcs_style_call = spi.read<1>();
Utility headers can be found within interface folders with the name util.hpp
.
For example if you want to use utilities for embed::adc
then you would
include #include <libembeddedhal/adc/util.hpp>
.
To keep the semantics consistent almost every driver will have either or both a
embed::read()
or embed::write()
free function. These functions should do
what a typical developer should expect, read from the device for input devices
or write to the device for output devices. The exact behavior depends on the
interface.
To find more, see the π Software APIs.
Instantiating a device driver is different from a peripheral driver because device drivers require other drivers in order to operate. Sensors tend to need i2c or spi. GPS modules generally require serial. Motor controllers generally need pwm signals and some output pins.
The following steps assumes you already have a project that can be compiled and flashed on to a device and also has driver support for i2c.
This particular examples uses the "normal" or low bandwidth versions of the mpu6050 driver and not the high bandwidth version that is more complicated.
In order to get started we need install libmpu6050 via conan.
conan install libmpu6050
TBD
Follow along with the example code below
#include <libexamplemcu/i2c.hpp>
#include <libmpu6050/mpu6050.hpp>
int main() {
embed::i2c & i2c = /* some i2c driver provided here */;
// Create an mpu6050 driver and pass the i2c associated with the physical i2c
// bus that is connected to the mpu6050's SDA and SCL lines.
embed::mpu6050 mpu6050(i2c);
// Get a reference to the accelerometer portion of the mpu6050
embed::accelerometer & accelerometer = mpu6050.as_accelerometer();
// Get a reference to the gyroscope portion of the mpu6050
embed::gyroscope & gyroscope = mpu6050.as_gyroscope();
// Read a sample from the acceleration of the mpu6050
auto accelerometer_sample = accelerometer.read();
// Read the rotational velocity of the mpu6050
auto gyroscope_sample = gyroscope.read();
/* Do other work... */
}
At this point you have a fully functional and available accelerometer and gyroscope drivers that your code can use.
Using soft drivers is no different than using a device driver. The only difference between device drivers and soft drivers is the fact that soft drivers are not associated with a particular device like an mpu6050 or esp8266. They are generic.
A useful soft driver that can be used when a target does not have an spi
peripheral or cannot use one of the available spi busses, is the
embed::bit_bang_spi
. "bit bang" refers to any method of data transmission that
employs software as a substitute for dedicated hardware to generate transmitted
signals or process received signals. embed::bit_bang_spi
implements the
embed::spi
interface using 2 embed::output_pins
and 1 embed::input_pin
.
Being software emulated this driver is far slower than using hardware driven spi.
#include <libembeddedhal/spi/bit_bang.hpp>
#include <liblpc40xx/output_pin.hpp>
#include <liblpc40xx/input_pin.hpp>
int main() {
// Get references to all of the pins you want to use for spi emulation
embed::output_pin & clock = embed::lpc40xx::output_pin::get<0, 1>();
embed::output_pin & data_out = embed::lpc40xx::output_pin::get<0, 2>();
embed::input_pin & data_in = embed::lpc40xx::input_pin::get<0, 3>();
// Get an output_pin and have it act like a chip select
embed::output_pin & chip_select = embed::lpc40xx::output_pin::get<0, 4>();
// Construct the bit_bang_spi object using the implementations above
embed::bit_bang_spi bit_bang_spi(clock, data_out, data_in);
std::array<std::byte, 4> payload = {
std::byte(0x11),
std::byte(0x22),
std::byte(0x33),
std::byte(0x44),
};
chip_select.level(false);
embed::write(bit_bang_spi, payload);
chip_select.level(true);
return 0;
}
This can go even further. You don't need to use pins directly connected to the micro-controller. You could even use pins from a device driver such as an I/O expander:
#include <libembeddedhal/spi/bit_bang.hpp>
#include <liblpc40xx/output_pin.hpp>
#include <liblpc40xx/input_pin.hpp>
int main() {
embed::i2c & i2c0 = embed::lpc40xx::i2c::get<0>();
embed::pca9536 io_expander(i2c0);
// Get references to all of the pins you want to use for spi emulation
embed::output_pin & clock = io_expander.get_as_output_pin<1>();
embed::output_pin & data_out = io_expander.get_as_output_pin<2>();
embed::input_pin & data_in = io_expander.get_as_input_pin<3>();
embed::output_pin & chip_select = io_expander.get_as_output_pin<4>();
// NOTICE: That the code below doesn't have to change even if the pin
// implementations change.
// Construct the bit_bang_spi object using the implementations above
embed::bit_bang_spi bit_bang_spi(clock, data_out, data_in);
// Get an output_pin and have it act like a chip select
std::array<std::byte, 4> payload = {
std::byte(0x11),
std::byte(0x22),
std::byte(0x33),
std::byte(0x44),
};
chip_select.level(false);
embed::write(bit_bang_spi, payload);
chip_select.level(true);
return 0;
}
Utility classes are like soft drivers except they do not implement hardware interfaces. Utility classes are generally used to manage an interface and extend a driver's usefulness.
Examples of this would be embed::can_network
which takes an embed::can
implementation and manages a map of the messages the device has received on the
can bus.
Another example is embed::uptime_counter
which takes an embed::counter
and
for each call for uptime on the uptime counter, the class checks if the 32-bit
counter has overflowed. If it has, then increment another 32-bit number with the
number of overflows counted. Return the result as a 64-bit number which is the
concatenation of both 32-bit numbers. This driver, so long as it is checked
often enough, can take a 32-bit hardware counter and extend it to a 64-bit
counter.
(TODO)
(TODO)
(TODO)
Errors are handled in libembeddedhal using Boost.LEAF. Check out their documentation for details on how to use it in detail. It is generally favorable to enable embedded mode for LEAF as it greatly reduces the storage and memory requires of the system.
// Define this at the top of your main application file or in your compiler
// arguments.
#define BOOST_LEAF_EMBEDDED
// If you aren't using threads then add this as well
#define BOOST_LEAF_NO_THREADS
LEAF also allows you to control how exceptions are handled by defining a
boost::throw_exception(std::exception const&)
function. In general you want
this to simply execute std::exit
when this occurs. To do this, simply add
this snippet to one of the C++ files linked into the project.
namespace boost {
void throw_exception(std::exception const& e)
{
std::exit();
}
#define BOOST_LEAF_EMBEDDED
#define BOOST_LEAF_NO_THREADS
#include <array>
#include <span>
#include <libembeddedhal/i2c/util.hpp>
#include <liblpc40xx/i2c.hpp>
int main()
{
// Get an i2c peripheral implementation
auto& i2c0 = embed::lpc40xx::i2c::get<0>();
// Default configure the i2c0 bus (100kHz clock)
i2c0.configure({});
boost::leaf::try_handle_all(
// First function can be considered the "try" portion of the code. If an
// error result is returned from this function the handlers below will be
// called.
[&i2c0]() -> boost::leaf::result<void> {
constexpr std::byte address(0x11);
std::array<std::byte, 1> dummy_payload{ std::byte{ 0xAA } };
// Functions that return boost::leaf::result must have their result
// checked and handled. To do this we use the BOOST_LEAF_CHECK to remove
// the boiler plate in doing this.
//
// To make sure that errors are transported up the stack each call to a
// function returning a boost::leaf::result must be wrapped in a
// BOOST_LEAF_CHECK() macro call.
BOOST_LEAF_CHECK(embed::write(i2c, address, dummy_payload));
return {};
},
// Functions after the first are the handlers.
// In this case, we only check for embed::i2c::errors.
[](embed::i2c::errors p_error) {
switch(p_error) {
case embed::i2c::errors::address_not_acknowledged:
// Handle this case here...
break;
case embed::i2c::errors::bus_error:
// Handle this case here...
break;
}
},
// A function that takes no parameters is the wild card and is called when
// there are unhandled remaining errors
[]() {
// Unknown error occurred!
// Handle those here!
});
return 0;
}
// This is here to remove exceptions from being thrown
namespace boost {
void throw_exception(std::exception const& e)
{
std::exit();
}
} // namespace boost
(TODO)
(TODO)
(TODO)
libembeddedhal uses tweak.hpp
header files for customization and
configuration. See A New Approach to Build-Time Library
Configuration
for more details.
#pragma once
#include <string_view>
namespace embed::config {
// Defaults to "test". Indicates that the current running platform is a
// unit/integration test. Change this to the target platform you are building
// for. For example, if you are targeting the LPC4078 chip, you should change
// this to "lpc4078".
constexpr std::string_view platform = "test";
// Defaults to "true". Enables stack tracing when errors do occur. There is a
// performance cost, albeit small, to capturing the current function name.
constexpr bool get_stacktrace_on_error = true;
// Defaults to "32". The maximum depth a stack trace can reach before it stops
// adding entries to the stack trace. Changing this effects the amount of space
// that the embed::stacktrace object takes up in a functions stack when used
// with Boost.LEAF.
constexpr size_t stacktrace_depth_limit = 32;
// Defaults to "false". If set to false, only the fully qualified function name
// will be stored in the stack trace. Set to true, the stack trace will capture
// the line number and file name into the stack trace object as well. Capturing
// the file names will increase the binary size of the application as the file
// name strings need to be stored in ROM.
constexpr bool get_source_position_on_error = false;
} // namespace embed::config
Create a libembeddedhal.tweak.hpp
file somewhere in your application and make
sure it is within one of the compiler's include paths. For GCC/Clang you'd use
the -I
flag to specify directories where headers can be found. The file must
be at the root of the directory listed within the -I
include path.
TL;DR: This technique is used to eliminate the cost of making virtual function calls.
Each interface in libembeddedhal uses the keyword virtual
to support runtime
polymorphism. There are consequences to using the virtual
keyword such as the
generation of a "vtable". This article
Demystifying virtual functions, Vtable and VPTR in
C++
explains how vtables work in detail.
Whether or not vtables use too much space for an application is up for debate depending on the application. libembeddedhal mitigates this by trying to keep the number of virtual functions for each interface as small as is reasonable.
The real concern regarding virtual
keyword use is the function call
performance. In order to call a virtual function, a lookup must be performed,
then the call can be made. This tends to require 1 to 2 additional instructions
before a function is called. For most applications this is negligible but for
those in which this is a deal breaker there is a solution in using the "Virtual
Static Polymorphism" technique. Note that this technique can improve call speed
but at the cost of increasing the binary size of the application.
Here is an example of a soft driver for embed::input_pin
which inverts the
value of the read function using VSP.
namespace embed
{
template<typename T = embed::input_pin>
class invert_read : public embed::input_pin {
public:
template<typename U>
invert_read(U & p_input_pin) : m_input_pin(p_input_pin) {}
private:
boost::leaf::result<void> driver_configure(
const settings& p_settings) noexcept override
{
return m_input_pin->configure(p_settings);
}
boost::leaf::result<bool> driver_level() noexcept override
{
return !BOOST_LEAF_CHECK(m_input_pin->level());
}
T * m_input_pin;
};
}
How is this useful? See the breakdown.
In this scenario, the default class template type has not been explicitly changed and thus the code will call class functions in a virtual, indirect way.
embed::some_mcu::input_pin & input0 = embed::some_mcu::get_input_pin<0>();
embed::invert_pin runtime_polymorphic(input0);
auto result0 = runtime_polymorphic.read();
The information about the original class object and its internal implementation
is not visible to the runtime_polymorphic
object. So when read is called,
because the type of the internal pointer is T = embed::input_pin
, the code
must perform a virtual call through the interface.
Now lets look at a scenario where the default class template type has been explicitly set to the type of the input pin driver.
// Uses static (direct) function calls
embed::some_mcu::input_pin & input1 = embed::some_mcu::get_input_pin<1>();
embed::invert_pin<embed::some_mcu::input_pin> static_polymorphic(input1);
auto result1 = runtime_polymorphic.read();
Now embed::invert_pin
is no longer dealing with an interface as type T
is
now embed::some_mcu::input_pin
. As far as embed::invert_pin
is concerned, we
never used an interface in this case. Note that the constructor's type U
is
now equal to the type T
and thus there is no down casting occurring. Now when
read is called, the class has full context regarding the implementation of the
read function. The compiler can then make a decision on whether or not to do the
following three options:
- Worst Case Scenario: virtual call (costly so unlikely)
- Better Scenario: if the function's implementation is sufficiently large, direct function call. This does result in cost but its better than a virtual call.
- Best Case Scenario: if the function's implementation is small enough, the compiler can inline the implementation of the driver into the soft driver removing the call entirely.
The PROS of scenario 2 or 3 from the list above is that you get better calling performance. And if you stack these multiple levels deep the performance improves stack.
The CONS of this is that for each different unique explicit instantiation of
embed::invert_pin
, there will be multiple implementations of the same driver
in the binary. For example, if a project has 3 drivers that implement the input
pin interface and each requires an embed::invert_pin
class to invert their
read values, then you would have the following:
// using virtual calls
embed::invert_pin<embed::input_pin>
// direct calls to some_mcu::input_pin
embed::invert_pin<embed::some_mcu::input_pin>
// direct calls to io_expander::input_pin
embed::invert_pin<embed::io_expander::input_pin>
The cost of all of these driver instantiations can be large for large projects if the choice of speed over space is not made carefully.
Peripherals are platform specific, thus an lpc40xx output pin driver will not work on an stm32f10x device. Because each implements the peripherals in a different and unique way there needs to be a separate driver for each. This is the crux of why embedded software is not portable across multiple devices. But libembeddedhal has a method of fixing this.
The following section of code will explain in its comments how to support multiple platforms between lpc40xx and stm32f103
int main()
{
// Step 1. Create a set of interface pointers to each driver your application
// will needed.
embed::input_pin * button{};
embed::output_pin * led{};
// Step 2. Map each pointer to their respective peripheral on either device.
// `if constexpr` is required to prevent leaking implementation
// details from one platform to another. This is important because a
// lpc40xx driver can never work on stm32f10 and thus leaking code
// into a binary meant for another platform results in code bloat.
if constexpr (embed::is_platform("lpc40"))
{
button = &embed::lpc40xx::input_pin<0, 1>();
led = &embed::lpc40xx::output_pin<0, 2>();
}
else if (embed::is_platform("stm32f10"))
{
button = &embed::stm32f103::input_pin<'A', 1>();
led = &embed::stm32f103::output_pin<'B', 2>();
}
else
{
return -1;
}
// Step 3. Use the interface pointers above.
while(true)
{
if (button.read().value())
{
(void)led.level(true);
}
}
}
The badges above are displayed in a library's README.md right below the title to indicate attributes of the library. When searching for a library to use for your project, these badges can help you decide if the project meets your requirements.
All libraries that implement libembeddedhal interfaces should have this badge to indicate library users/consumers.
If a library follows completely the AUTOSAR C++20 guidelines.
This badge is placed for a libraries that have the possibility to dynamically
allocates memory via new
, malloc
, std::allocator
or a standard library
that uses any of the allocating functions.
If a library uses floating point arithmetic anywhere in its implementation. Some points to consider when seeing a library that uses floats:
- Floating point operations take up flash space for devices without a hardware FPU (floating point unit).
- The cost is only paid once because the software floating point support code can be called each time for each operation. So if you are already using software floats then it is likely that using this library will not add more space to a project.
- Software driven floating arithmetic is very slow compared to integer arithmetic.
- For applications using multiple threads along with FPU support, context switching time will increase because the FPU registers will need to be saved.
If a library ever throws an exception anywhere in its code base. Some points to consider when seeing a library that uses floats:
- Exceptions can be quite slow to propagate when they occur, although this is not always the case.
- If a project has exceptions turned off, this library will not compile, and thus cannot be used.
- Using exceptions generally incurs an increase in size for your binary (around 8kB for the exception runtime environment for arm cortex-m).
- The infrastructure for error handling is already handled by Boost.LEAF so exceptions are redundant.
- Exceptions require dynamic memory allocate.
All guides follow the C++ Core Guidelines.
In order to demonstrate how to create an interface a thoroughly documented/commented set of example code has been written. The comments in these files acts as guides to help new developers learn how to create their own libembeddedhal interfaces, peripheral drivers, device drivers and soft drivers.
The following files are a guide to how to write their respective driver:
example/interface.hpp
example/peripheral_driver.hpp
example/device_driver.hpp
example/soft_driver.hpp
Listed below are the policies that every libembeddedhal implementation must follow to ensure consistent behavior, performance and size cost:
- Code shall follow libembeddedhal's
.clang-format
file, which uses the Mozilla C++ style format as a base with some adjustments. - Code shall follow libembeddedhal's
.naming.style
file, which is very similar to the standard library naming convention:- CamelCase for template parameters.
- CAP_CASE for macros.
- lowercase snake_case for everything else.
- prefix
p_
for function parameters. - prefix
m_
for private/protected class member.
- Refrain from variable names with abbreviations where it can be helped.
adc
pwm
andi2c
are extremely common so it is fine to leave them abbreviations. Most people know the abbreviations more than the words that make them up. But wordscnt
should becount
andcdl
andcdh
should be written out asclock_divider_low
andclock_divider_high
. Registers do get a pass if they directly reflect the names in the data sheet which will make looking them up easier in the future. - Use
#pragma once
as the include guard for headers. - Every file must end with a newline character.
- Every line in a file must stay within a 80 character limit.
- Exceptions to this rule are allowed. Use // NOLINT in these cases.
- Radix for bit manipulation:
- Only use binary (
0b1000'0011
) or hex (0x0FF0
) for bit manipulation. - Never use decimal or octal as this is harder to reasonable about for most programmers.
- Only use binary (
- Every public API must be documented with the doxygen style comments (CI will ensure that every public API is documented fully).
- Include the C++ header version of C headers such as
<cstdint>
vs<stdint.h>
.
- Use the
libxbitset
library to perform bitwise operations operations. - Only use macros if something cannot be done without using them. Usually macros can be replaced with constexpr or const variables or function calls. A case where macros are the only way is for BOOST_LEAF_CHECK() since there is no way to automatically generate the boiler plate for returning if a function returns and error in C++ and thus a macro is needed here to prevent possible mistakes in writing out the boilerplate.
- Only use preprocessor
#if
and the like if it is impossible to useif constexpr
to achieve the same behavior. - Never include
<iostream>
as it incurs an automatic 150kB space penalty even if the application never uses any part of<iostream>
. - Drivers should refrain from memory allocate as much as possible that includes
using STL libraries that allocate such as
std::string
orstd::vector
. - Logging within a library is prohibited for two reasons:
- String formatting libraries may not be the same across libraries and an application including both will have to pay the space cost for two separate formatting libraries.
- libembeddedhal libraries do not have the right to output to stdout/stderr, that is the role and responsibility of the application.
- Interfaces must follow the public API, private virtual method shown here.
- Inclusion of a C header file full of register map structures is not allowed as it would pollute the global namespace and tends to result in name collisions.
- libarmcortex: Drivers for the ARM Cortex M series of processors.
- libriscvi32: Coming soon. Drivers for 32-bit RISC-V processors
- liblpc40xx: Drivers the lpc40xx series of microcontrollers. This includes startup code, linker scripts, and peripheral drivers.
- libstm32f1xx: Coming soon. Drivers for atmega328 micro-controller
- libesp8266: Drivers for the esp8266 micro-controller as well as drivers for the WiFi/Internet firmware AT client.
- libmpu6050: Coming soon. Accelerometer and gyroscope device. Requires an i2c driver.
- libatmega328: Coming soon. Drivers for atmega328 micro-controller
The world of embedded systems is written almost entirely in C and C++. More and more the embedded world moves away from C and towards C++. This has to do with the many benefits of C++ such as type safety, compile time features, meta-programming, multiple programming paradigms which, if use correctly can result in smaller and higher performance code than in C.
But a problem for embedded software in C++, as well as in C, is that there isn't a consistent and common API for embedded libraries. Looking around, you will find that each hardware vendor has their own set of libraries and tools for their specific products. If you write a driver on top of their libraries, you will find that your code will only work for that specific platform/product. In some cases you may also be limited to just their toolchain. You as the developer are locked in to this one specific setup. And if you move to another platform, you must do the work of rewriting all of your code again.
libembeddedhal seeks to solve this issue by creating a set of generic interfaces for embedded system concepts such as serial communication (UART), analog to digital conversion (ADC), inertial measurement units (IMU), pulse width modulation (PWM) and much more. The advantage of building a system on top of libembeddedhal is that higher level drivers can be used with any target platform whether it is an stm32, an nxp micro controller, an RISC-V or is on an embedded linux.
This project is inspired by the work of Rust's embedded_hal and follows many of the same design goals.
libembeddedhal's design goals:
- Serve as a foundation for building an ecosystem of platform agnostic drivers.
- Must abstract away device specific details like registers and bitmaps.
- Must be generic across devices such that any platform can be supported.
- Must be minimal for boosting performance and reducing size costs.
- Must be composable such that higher level drivers can build on top of these.
- Be accessible through package mangers so that developers can easily pick and choose which drivers they want to use.
If you find an issue you'd like to work on, simply type and submit a comment
with the phrase .take
in it to get assigned by our github actions.
Submitting a PR at anytime is fine, but code will not be reviewed until all of the continuous integration steps have finished.
Commit message style:
Title no more than 50 characters
<empty newline here>
Actual content of the commit message in the present tense.
A pull request should only have a single commit in it at the start. For each round of review, additional commits can be made, but this is not required.