Code and Documentation for USST’s first Canadian CubeSat Project: The RADSAT-SK Satellite
- Setting Up Your Repo
- How to Contribute
- Branching
- Directory Structure
- Coding Standard
- Code Documentation
- Get WSL (Windows Subsystem for Linux) or Git Bash for your computer
- Using one of the aforementioned programs, navigate to where you'd like the repository to exist
- Run
git clone https://github.com/USST-RADSAT-SK/software-and-command.git
(downloads the repository to your computer) - Navigate to the repo:
cd software-and-command
- Run
git config core.hooksPath .githooks
(sets where git looks for the githooks) - Run
chmod +x .githooks/pre-commit
(makes the githook script executable)
Now your repo should be all set up! Check out our "How to Contribute" and "Branching" sections below and coordinate with the Software and Command Team Lead(s) for further guidance.
Issues are how we track software development tasks. Issues are typically either feature or bug related. E.g. "Implement Payload Collection Task" or "Fix I2C Bug". Issues can be created by going to the "Issues" tab within GitHub, and selecting "New Issue". Be sure to coordinate with your Team Lead(s) if you're unsure about this process though.
Be sure to assign the appropriate Project to the Issue being created; e.g. for an I2C bugfix, that would likely go into the "Framework" Project. If you're unsure of what projec it goes under, contact your Team Lead(s).
GitHub has a "Projects" tab, up top near the "Issues" tab. A Project is essentially a KanBan board that tracks individual Issues (and PRs). Issues and PRs can either be "To-Do", "In Progress", "In Review" or "Done". If you're looking for something new to work on, take a look at the items in the "To-Do" list of any Project! All you have to do is drag the Issue into the "In Progress" state. Be sure to communicate with the Software Team and Team Lead if you're not super familair with the process. Don't forget to assign yourself (and anyone else you're working with) on the Issue as well, so the Team knows what you're working.
When you finish (your first attempt at) the task, create a Pull Request of your working branch into alpha. Also move your issue from the "In Progress" state into the "In Review" state. Communicate with the software Team and Team Lead, and your Team Lead will facilitate reviewing and approving the PR and placing the task into the "Done" state.
We have 5 "levels" of branching used:
- master -> This is reserved for FLIGHT READY code, i.e. very well tested.
- beta -> This is reserved for FLATSAT code, i.e. moderately well tested.
- alpha -> Working development branch, reserved for code that has been at least partially reviewed / tested.
- other -> These branches are the only places where development happens. Each of these is based off of alpha.
- hotfix -> These are quick, one-off branches intended for quick fixes that are found on alpha or beta branches.
Anyone can create a branch off of alpha and start developing. However, 2 people are required to review a PR (Pull Request) before the code can be accepted into the alpha branch. One of these people must be a team lead or project manager.
alpha can be merged into beta, and beta can be merged into master. These are done very seldom throughout the project as the codebase matures, and may ONLY be done by the Software and Command Team Lead(s) or CubeSat Project Managers.
- In your local repo run
git checkout alpha
(you may have to commit, stash, or throw away uncommitted changes on your current branch) - Run
git pull
(makes sure you have the latest code) - run
git checkout -b "<your branch name>"
(creates a new branch) - The first time that you try to push on the branch it will throw an error. Just follow the instructions to set the upstream branch.
All branches MUST follow the few branch naming rules. Those rules are:
- No captials
- No underscores
- Use hyphens instead of spaces
- Must prepend new branch into one of six directories (see below)
GitHub (and most other Git platforms) allow you to use branch folders, simply by uses forward slashes. Some examples of good branch names:
admin/restructure-directories
test/write-unit-test-framework
application/implement-payload-collection-task
operation/import-nanopb
framework/implement-uart-wrapper
hotfix/fix-i2c-bug
Notice that all six of the directories used are based off of the Project names for the RADSAT-SK GitHub repo (minus hotfix, which is for quick fixes on alpha or beta branches).
When bringing a new feature into flight code, you must merge your feature branch into the alpha branch after a review process know as Pull Requests or PR. To create a PR from your local machine:
- Check that all of the edited files that you want to be included with your pull request are being tracked by running
git status
- You can add files to the commit tracking in several ways:
- Add files individually
git add path/to/file.c
- Add files using wildcards (ex: all files in sub directory)
git add path/to/files/*
- Add all files that have tracked changes
git add -A
- Add files individually
- You can add files to the commit tracking in several ways:
- Commit your local changes using
git commit -m "Commit message"
- Checkout the alpha branch using
git checkout alpha
- Get any remote updates from the remote alpha using
git pull
- Checkout your local branch using
git checkout <branch-name-you-want-to-PR>
- Merge the remote changes to alpha into your local feature branch using
git merge alpha
- This will update your local branch with any changes that were made to the alpha branch since you started working on your feature.
- Resolve all merge conflicts through the terminal or your IDE. These are where your branch and the current alpha branch are different and you must decide what is used in your PR. Conflicts will be marked similar to this:
<<<<<<< HEAD (Current Change) This is an edit on your current branch ======= This is an edit on the alpha branch >>>>>>> alpha (Incoming Change)
- Incoming changes are those from the alpha branch and the current changes are what's on your feature branch.
- As a general rule of thumb, if your branch doesn't affect a section with a merge conflict, accept the incomming change from
alpha
. - After your conflicts are resolved, commit your local feature branch using
git commit
- Push your local branch to the remote repository using
git push
- To make your pull request, go to the remote repository on your browser. You should see a prompt above the directory structure prompting you to make a PR from a recent branch push. Click on that prompt or go to the "Pull Requests" tab and select "New pull request".
- Once you've started your PR, Give it a meaningfull name and a comment on what is in the PR. If your PR is associated with any issues, in the comment section, type
close #
followed by the name or number of the issue(s). This will automatically close the issue(S) once the PR is approved and keeps the repo clean. - Add Reviewers to the PR to notify them to look it over and get feedback. We require at least two reviewers, but you can assign however many extra you want.
- Assign yourself and any of your co-workers to the PR to stay up-to-date on the feedback and so we know who to ask about it.
- Finally click "Create Pull Request"
- All PRs will have to be explained at the next team meeting where you and your co-workers will go over what the prupose of the PR is, what design choices you made, and why you made them.
We have chosen to follow a layered approach to code organization, partitioning our project into six Layers. From the top-down:
- Application -> Performs specific functions required by the mission. Contains
main()
function - Operation -> Provides generic operations that support the mission
- Framework -> Interfaces with the OS & HAL, to support the mission operations
- OS -> Wraps the Hardware to provide kernel-level support (task scheduling, semaphores, etc.)
- HAL -> Abstracts (i.e. simplifies) access to OBC hardware and peripherals
- Hardware -> The actual On-Board Computer's hardware and peripherals
Each layer provides an API to the layer directly above it, and thus each layer only interfaces with the layer directly beneath it. For example, the code within the Operation layer only uses the functionality provided by the Framework layer. This allows for nice encapsulation and de-coupling between the layers.
This has multiple benefits. One is that it allows for easier testing, since the top two layers should be completely hardware-independent, meaning they can (for the most part) be unit tested on a desktop PC. The decoupling also reduces complexities and debugging time, since each layer can only interface with the one directly beneath it. This can also be used to assist with deadlock prevention and other concurrency issues, since the lower layers (framework, OS) can be responsible for that.
It is important to note that the RADSAT-SK team is only developing the top three layers; the bottom three have already been provided to us.
If you're unsure of where to place some new code, talk to the current Software and Command Team Lead(s).
Our coding standard is loosely based on the Qt coding style found here.
We have a coding standard so that everyone's code looks the same and is easily readable. Commits made to the project not adhering to these standards may not be allowed to be pushed. Source code from a third party will not be expected to follow these standards.
Like all rules, some exceptions can be allowed. The most important takeaway is that your code should be consistent and easy to read.
Tabs or 4 spaces are allowed.
Variable names should be descriptive and abbreviations should almost always be avoided. Exemptions may apply to loop variables:
uint16_t sum = 0;
for (uint16_t i = 0; i < maxCount; i++) {
sum += i;
}
All variable and function names are in camel case (first word lowercase, follwing words capitalized):
uint16_t myNewVariable;
This is true for most variables and constants. However, for macros (and some enumerable types), the name is in all caps with underscores in between words:
#define ARRAY_SIZE ((uint8_t)8)
enum booleanValues {
FALSE = 0,
TRUE = 1,
};
As seen above, make sure to always wrap macros in brackets, and explicitly cast their type.
Most enums will have "global" scope, so you'll usually want to prepend their enumeration names with the name of the enum itself:
enum colours {
colourRed = 0,
colourGreen = 1,
colourBlue = 2,
};
ALso note that the enumeration values are all explictly defined; this is highly recommend for readability and to prevent mistakes.
In functions, most variables that will be used throughout the function should be declared at the top of the function. Exceptions may include variable declarations within the scope of an if or for loop.
Use of "non-standard" c types (char
, int
, long
) should be avoided whenever possible. In embedded programming, it is always recommended to use explicit types. It's clearer to the user/reader, and consistent across all platforms. However, signed types do have their uses; e.g. the HAL and SSI libraries use int
for return types (error codes), so it's fine to use them when working directly with those libraries.
Standard c types (also called fixed-width types, or explicit types) include:
uint8_t
(instead ofunsigned short
orunsigned char
)uint16_t
(instead ofunsigned int
)uint32_t
(instead ofunsigned long
)
Remove the "u" prefix to use a signed version of the above types when signed values are needed.
We only have 1 system to worry about with our project (the OBC), so portability isn't a huge concern, but it's still good practice to use explicit types whenever reasonable and possible. IF YOU'RE UNSURE OF WHAT TO USE: just use fixed-width (standard) c types, as listed above.
To prevent namespace collisions and to make it extra obvious what code is "local" (rather than imported), all locally created files MUST be prepended with the R character. After that, they follow the CapitalCase convention (each word starts with a capital, everything else is lowercase). Absolutely no underscores or hyphens in file names.
Names should also be short and sweet. Acronyms are fine, but are still subject to CapitalCase conventions.
Some good examples:
- RProtobuf.h
- RPayloadCollectionTask.c
- RUart.h
- RDosimeter.h
Every single source and header file written for the RADSAT-SK cubesat needs a Doxygen file header of the following style:
/**
* @file RProtobuf.c
* @date March 20 2021
* @author Jim Lahey (jhl211)
*/
Including your full name and NSID is important in case the team ever needs to contact someone about a piece of code that they wrote.
To increase readability (especially in larger files), multi-line function separators should be used. Ideally, these are used in all files. Do not use the separators to define a section if the section is empty, however. See the main examples of sections that are used:
/***************************************************************************************************
DEFINITIONS
***************************************************************************************************/
/***************************************************************************************************
PRIVATE FUNCTION STUBS
***************************************************************************************************/
/***************************************************************************************************
PUBLIC API
***************************************************************************************************/
/***************************************************************************************************
PRIVATE FUNCTIONS
***************************************************************************************************/
Each line ends after exactly 100 characters, and the words are centered. These are not strictly enforced, but are highly recommended. Consistency is the most important thing.
In function definitions and function calls, no additional whitespace is needed.
uint16_t myFunction(uint16_t arg1, uint16_t arg2) {
myOtherFunction(arg1, arg2);
}
If, switch, for, and while statements have a similar style. But make sure to leave a space around the "for" keyword and the curly brace. Additional whitespace can be used when necessary, but it usually isn't.
for (uint8_t i = 0; i < maxCount; ++i) {
if (i == 0) {
i = maxCount;
}
}
In between function definitions, exactly two lines of whitespace should be used.
uint16_t myFunction(uint16_t arg1, uint16_t arg2) {
return myOtherFunction(arg1, arg2);
}
uint16_t myOtherFunction(uint16_t arg1, uint16_t arg2) {
return (arg1 + arg2);
}
Within a function, one line of whitespace should separate functional "chunks" of code. Two lines may be used when things get crowded, however if you feel the need to partition your function like this, it may be time to split it into multiple functions, or use inline comment blocks to separate them:
uint16_t myFunction(uint16_t arg1, uint16_t arg2) {
// init
uint16_t newVariable = 0;
uint16_t otherVariable = 0;
// do thing
newVariable = arg1 + 1;
/* Now do the other thing */
// bar bar
newVariable += 1;
otherVariable = myOtherFunction(newVariable);
return otherVariable;
}
Whitespace can sometimes be helpful or even necessary to increase legibility. Feel free to use additional whitespace (within reason) where you see fit.
When declaring a pointer variable, the asterisk goes right after the variable type, then a space is left between the asterisk and the variable name:
int main(uint16_t argc, int8_t* argv[]) {
uint16_t* myArray = (uint16_t*)pvPortMalloc(ARRAY_SIZE * sizeof(uint16_t));
}
When using operators with a single operand (like address-of), there is no space between the variable and the operator. When using operators with 2 (or 3) operands, the operator is wrapped with spaces:
uint16_t myInt = 0;
uint16_t* myIntPtr = &myInt;
myIntPtr = myIntPtr + 4;
*myIntPtr = *myIntPtr * 2;
For all conditional statements, loops, and function definitions, the opening curly brace goes on the same line as the conditional logic/function header.
uint16_t myFunction(void) {
uint16_t i = 0;
while (i++ < maxCount) {
printf("%d", i);
}
}
Note that if a conditional statement has only a single line of code as a body, curly braces are still required and the line of code still goes on the next line for maintainability and readability. Else-if and else blocks have a closing curly brace, then the else keyword goes on the next line:
if (counter == 0) {
return 1;
}
else if (counter == 1) {
return 0;
}
else {
return counter;
}
If there is any ambiguity to the order of operations in your expression, use parentheses (and additional whitespace) to make the order of operations explicit:
uint16_t errorResult = ( ( i & 1 ) * 4 ) + ( i & 3 );
Cases that simply fall into the following case should be grouped together. Cases that do something and intentionally fall into the next case should explicitly say so with a comment. All case statements must end in either a break or return. Whitespace newline is left between each distinct set of cases. See example below.
uint16_t function(uint16_t n) {
uint16_t returnValue = 0;
switch (n) {
case (0):
case (1):
case (2):
returnValue += doExtraThing(n);
// FALLTHROUGH
case (3):
returnValue += n;
break;
default:
return 0;
}
return returnValue;
}
Lines should aim to be 80 characters or less long, but the maximum accepted line length will be roughly 100 since no one really uses terminals anyways. Some exceptions may be made, but anything over 100 lines is starting to push the limits of readability.
Functions that wish to return an "error code" (e.g. a value that represents if the function was successful in its operation) should follow the following format: Return type: int Return value: 0 for success, non-zero for failure. Reference the HAL, SSI, or other underlying files for more info if the error code does not originate in the function you're describing. An example is shown below:
/**
* @brief Write data into the FRAM peripheral.
* @param data The pointer to where the data will be copied from.
* @param address The FRAM address to begin writing data to.
* @param size The number of bytes to copy into the FRAM peripheral.
* @return 0 for success, non-zero for failure. See hal/Storage/FRAM.h for details.
*/
int framWrite(uint8_t* data, uint32_t address, uint32_t size) {
int error = FRAM_writeAndVerify(data, address, size);
return error;
}
Note that for functions which use their return values for other purposes (e.g. returning a calculated value) or simply return void
, this section can be ignored.
Our code is documented using doxygen. All comments used for documentation need to be in comment blocks starting with /** and ending with */ otherwise Doxygen will not recognize them.
The documentation for functions should be put in the source (.c) file that the function is defined in.
/**
* A short one line description
*
* The detailed description is often not necessary, and should be used sparingly.
* It can be multiple lines long. Try not to get too technical; keep the description
* high-level, in case the inner-workings of the function are ever changed.
*
* @note give a notice to anyone using this function (if any; usually not)
* @pre describe the pre condition (if any; usually not)
* @post describe the post condition (if any; usually not)
* @param input short description of the input parameter
* @return describe the return value
*/
uint16_t function(uint8_t input) {
// code
}
Another real example is shown below:
/**
* @brief Write data into the FRAM peripheral.
* @param data The pointer to where the data will be copied from.
* @param address The FRAM address to begin writing data to.
* @param size The number of bytes to copy into the FRAM peripheral.
* @return 0 for success, non-zero for failure. See hal/Storage/FRAM.h for details.
*/
int framWrite(uint8_t* data, uint32_t address, uint32_t size) {
int error = FRAM_writeAndVerify(data, address, size);
return error;
}
Documentation for global variables should go inside the source (.c) file that they are defined in.
/** short description of the variable */
uint16_t variable;
Documentation for typedefs should go inside the header (.h) file that they are defined in.
/** short description of the typedef */
typedef uint8_t CHARACTER;
/** short description of the struct */
typedef struct _myStruct {
uint16_t a; /**< short description of the member */
uint8_t b; /**< short description of the member */
} myStruct;
/** short description of the enum */
typedef enum {
TRUE, /**< short description of the member */
FALSE, /**< short description of the member */
} BOOL;
/** short description of the macro */
#define myMacro 1
/**
* short description of the macro
* @param x short description of x
* @param y short description of y
*/
#define functionMacro(x, y) (x + y)