Software Design & Structure

Software Design & Structure

like how to software ig…disclaimer half of ts is chat so use ur brain

Repo Setup

When creating a new Github repo, you should add a branch ruleset to require two required approvals to merge into main. Go to your repository’s settings page and configure the ruleset like this:

image-20250608-225710.png

PR template?

CI/CD? ideally we should setup a build workflow at least. theres prob a way to github template with build actions…

File Structure

Top-level file structure for software repos should look something like this:

  • Apps - contains main.c, error handlers, any high-level common functionality

  • Drivers - contains low-level device drivers

  • Embedded-Sharepoint (submodule) - contains BSP, linker files, flash script, etc

  • Tasks - contains FreeRTOS tasks

  • Tests - contains unit & integration tests

  • .gitignore

  • Makefile

Each of Apps, Drivers, and Tasks should have Src (source) and Inc (include) folders

Embedded-Sharepoint should almost always be checked out to main unless there’s a reason you need a particular branch (untested features, etc). Your gitignore (example) should be set up for C and set to ignore the Build directory to avoid committing build artifacts. We also have an example Makefile to get started which should build/flash your code with relatively few changes.

Make sure to change the PROJECT_TARGET variable to your chip (e.g. stm32f446ret)

Naming

Drivers should generally be named after the device they’re written for, such as “EMC2305.c” or “ADS131M08.h.” Tasks are where your code’s logic should be, they are scheduled to execute or suspend based on priorities and some magic . Name tasks using the convention FunctionTask.c, for example “InitTask.c” or “TemperatureMonitorTask.c.” InitTask is special in that it initializes all the other tasks and then deletes itself. Finally, in order to run tests using the Embedded-Sharepoint Makefile, they must be named like Test_Type_Name.c, for example “Test_Driver_Contactors.c” or “Test_Task_Timers.c.”

For reference, a good example repo is GitHub - lhr-solar/BPS-Leader: Reads voltage, temperature, and current of the main battery pack from the VoltTemp and Amperes boards to ensure the battery remains in a safe state.. However, use your discretion and make changes as needed to fit your board/system’s requirements.

Branching

Most branches fall into one of these categories:

  • Feature - adds new functionality to a release (merge into Release)

  • Bugfix - addresses bugs found when developing a release (merge into Release)

  • Hotfix - addresses bugs found in production/released environment (merge into main)

  • Release - adds a new major version of the software (merge into main)

Branch names should be formatted as Type/Name, for example “Feature/NewPinout” or “Hotfix/CAN-Init.” Examples of release times could be unveiling, safety verification, powered run, competition, etc.

Testing

Unit testing is a software testing technique where individual components or functions of a program are tested in isolation to ensure they work as expected. Each test checks a small, specific part of the code—typically a single function—with controlled inputs and expected outputs. This helps catch bugs early, improves code reliability, and makes future changes safer by verifying that existing functionality remains correct. - chat, 2025

Basically, you should write unit tests for every piece of major functionality in your code. As a byproduct, this encourages writing modular code that can be run and tested independently of other parts of the system. For example, we write tests for device drivers, CAN message integration, state machines, etc. When testing on hardware, you should use LEDs or another physical indicator of the test succeeding/failing.

Integration testing checks how different pieces of a system work together. Unlike unit tests, which isolate functions or modules, integration tests verify that combined components—like functions, classes, or external systems—interact correctly. These tests help catch issues that arise from incorrect assumptions between parts of the system, such as data mismatches or communication errors. - chat

Extensive examples of unit/integration tests can be found in the Daybreak BPS and Controls software repos. Keep in mind these were written for Micrium RTOS so there are some differences from the current FreeRTOS architecture.

Review Process

When you feel like your code is ready for review, make a pull request to merge your changes into the relevant release branch. In your PR notes, describe what you added/changed/fixed and any remaining todos or questions for the reviewers.

Before requesting a review, make sure to test on hardware and confirm functionality

You should assign reviewers in Github, which sends them an email, but make sure to drop your PR link in the #review-request slack channel and mention your 2-3 reviewers there as well.

Code review is an ongoing process - don’t expect to be able to merge the first time, you’ll probably have to make changes and iterate a few times before getting approvals

Releases

Software is ready to be released when it has been approved by peer-reviewers, merged into main, and tested in a hardware-in-the-loop (HITL) environment with other relevant boards/systems. Tagging a release means we’re confident that code can be run reliably in a safety-critical environment and will behave as expected by other systems. This also helps us keep track of what version of our code is running on the car at any time.

To tag a release on Github, follow this guide and make sure to add a meaningful description.

idk what we wanna do for version numbers? can do semver

Documentation

Every repo should have a readme - if you’re working on a dedicated firmware repo, this should be in the top-level directory, otherwise it should be in your Firmware directory. At a minimum, you should include a few sentences on what the code does, how it interacts with other boards (CAN/other signals), and how to build and run tests on hardware.

All significant (think if a new freshman couldn’t understand) functions should be documented above the function signature as follows:

/** * @brief Description of what the function does and any notes * @param ParameterName Description of the parameter * (If needed - Possible values for the parameter) * @return The return value of the function */

Examples:

/** * @brief Helper function for reading precharge completion state * Should only be called if contactor is a precharge contactor * @param contactor the contactor * (MOTOR_PRECHARGE_CONTACTOR/ARRAY_PRECHARGE_CONTACTOR) * @return The contactor's precharge completion state from the output of hardware comparison */ bool getPrecharge(contactor_enum_t contactor) { // blah blah }
/** * @brief Writes data to a specified pin * @param led The led to write to * @param state true=ON or false=OFF * @return None */ void Status_Leds_Write(status_led_t led, bool state) { // blah blah }

 Related articles