To keep all of the code organized, we follow a strict object-oriented design pattern that exposes only certain things to our top-level application code. The code that we execute can be thought of as broken into many layers, each corresponding to a set of files in our codebase.
At a high level, our code can be organized into a few basic categories, from top to bottom:
Application - Each board that we work on gets its own repository and implementation that depends on EVT-core. All application-specific code lives in these repositories to avoid cluttering those projects.
EVT-core - Central library that provides functionality needed by all the applications developed by the team
Device Drivers (dev/) - Some devices are used commonly across EVT projects, so it makes sense to store these drivers in EVT-core, instead of duplicating them across projects. Some of these drivers rely on the I/O layer, while others are for internal devices that don't require external communication.
Input/Output (io/) - This code simplifies communication with the microcontroller by wrapping HAL functions in easy-to-use C++ classes.
Hardware Manager (manager.hpp) - This file holds functions that statically allocate instances of EVT-core drivers, according to the current available hardware.
Utilities (utils/) - This group is made up of miscellaneous useful classes that don't fit nicely into other categories.
Hardware and Hardware Abstraction Layer (libs/HALxx) - Each series of microcontrollers released by STM is released with a HAL that can be used for that series. The HAL handles the low-level task of setting registers in the hardware, so we don't need to handle that ourselves.
In reality, our software architecture is much more complex than what is described above. As you work more with EVT-core, it will be important for you to understand what code you are developing and how it fits into the broader hierarchy.
To the right is a detailed diagram representing the team's software architecture. We have separated the majority of our architecture on the left from our real-time architecture on the right. We did this because the C++ Conversion Layer interacts with many other components, so it would be difficult to accurately depict.
The only software used by our team not shown in this diagram is the C/C++ Standard Library, excluded because it is used by every component.
Microcontroller Hardware Series - At the end of the day, all the code we write needs to be run on an MCU to actually perform the job we've written it for. STM produces hundreds of microcontrollers with different features and specs. For the purposes of organizing our software, we care about this at the series level because each series of microcontrollers gets its own Hardware Abstraction Layer (HAL). Examples of series are STM32F3, STM32L4, and STM32H7.
Hardware Abstraction Layer (libs/hal) - This is the lowest-level software layer, which communicates directly with MCU hardware. It manages all the hardware resources, setting values into bare-metal registers, so we don't have to. This layer attempts to expose the full functionality of the hardware, so its API is complex and extensive. EVT-core provides less functionality than the HAL, but its API is much more concise and tailored to our goals as a team, so it's easy for us to use.
Platform Abstraction Layer (pal/) - While the HAL abstracts away the details of working with the hardware on a particular series of microcontrollers, it is still tightly coupled to that series. The PAL goes one step further, abstracting away the details of the HAL to meet the standard EVT-core APIs. This allows us to run the same application-level code on microcontrollers of different series without having to modify it.
Input/Output (io/) - This component defines the API for each external communication method. It also defines some high-level functions that don't require hardware-specific knowledge. For example, the I2C class implements reading and writing registers by calling the read and write functions implemented in the PAL.
Device Drivers (dev/) - This component has two roles. First, it defines APIs for some peripherals inside the MCU, similar to the I/O component. Second, it provides a further layer of abstraction around the I/O component for specific devices. For example, if we want to add functionality for a pump controlled via PWM, a device driver would handle the specific I/O calls, which only exposes methods for high-level actions like setting the speed. Device drivers that are common to multiple boards are kept in EVT-core, while drivers for devices that are used only by a single board are kept in that board's repository.
Utilities (utils/) - This component builds on a number of other components to provide useful functions and data types. Its representation in the diagram above is not perfectly accurate, as some utilities rely directly on the I/O components and some rely only on the C/C++ Standard Library.
CANopen Stack (libs/canopen/) - The CANopen stack is a third-party library that we use to perform our CANopen communication. We give it a pointer to the data we want to exchange, and it handles the many complexities of that protocol. You can read more about that project on their site.
CANopen Abstraction Layer (io/canopen/) - Abbreviated COAL, this layer provides a few utility functions and number of macros to simplify the CANopen Stack's interface. The initialization code and object dictionary needed for its API can be very long and difficult to understand without COAL.
Hardware Manager (manager.hpp) - This file was created to handle the frustrating task of creating instances of PAL drivers without exposing the implementation to the application code. Normally, this could be done simply with a factory method, but because we don't use dynamic allocation, a factory method is not an option. Instead, this file uses a complex workaround requiring template functions, enum classes, and static local variables.
For this section, we'll assume the board that this application is being developed for has the acronym BRD.
Helper Classes (BRD/) - These are classes not directly tied to implementing a particular hardware driver but to handle some portion of the functionality of a board. They may or may not depend on EVT-core functionality.
Device Drivers (BRD/dev/) - Many of our boards use hardware components specific to that board. To avoid cluttering EVT-core with drivers that are used only on one board, we keep drivers for these board-specific components at the application level.
Board Class (BRD/BRD.hpp) - This class is responsible for operating a board to achieve its intended purpose. It often stores instances of all the necessary device drivers and helper classes to accomplish this.
Application (targets/BRD-BIKE/main.cpp) - This file is responsible for initializing the system and running the program's main loop.
ThreadX (libs/threadx/) - ThreadX is a free and open-source, safety-certified real-time operating system (RTOS). This system allows us to manage large applications in a safe, repeatable manner. You can read more about that project on their site.
C++ Conversion Layer (rtos/ccl/) - This layer wraps the ThreadX library with C++ functions and classes, so we can integrate it more seamlessly with our existing C++ codebase.
Thread-safe Wrappers (rtos/wrappers/) - These wrappers enhance the functionality of I/O and device drivers to ensure they can be run safely while using an RTOS. Some of these wrappers are implemented at the EVT-core level to provide common functionality. Others can implemented at the application level as needed. This component interacts directly many components in EVT-core.
Real-Time Application (targets/BRD-BIKE/main.cpp) - These applications differ significantly from applications that are not real-time. They do initialize the system, but they are also responsible for creating and managing multiple threads.