Skip to content

ORCAASAT Dev Guide

Richard Arthurs edited this page Oct 3, 2018 · 12 revisions

Intro

This development guide outlines the software architecture and directories. It provides examples and templates to serve as a starting point during feature development. Since the core goal of the OBC firmware is reliability, all software must remain consistent with the information in this document.

Managing Multiple Hardware Targets

Managing different hardware "targets" (hardware versions) is difficult because a lot of code is closely tied to the specifics of the hardware. To make this easier, we do 3 things:

  1. Organize the directories so that hardware versions are isolated
  2. Use build configurations to automatically enable only the required files and folders
  3. In our application layer code, use feature-specific names for peripherals, then map them to platform-specific names in the hardwaredefs.h file.

The next three sections describe these methods in more detail.

Directory Structure

There are 3 main directories (or directory prefixes) in the project structure.

Below is a simplified tree of the directory structure. Many subdirectories are hidden in this view because they aren't super important or they will change. A few important files are also shown.

├── orcasat
│   ├── examples
│   ├── test_sequences
│   └── unit_tests
│   └── drivers
│   └── filesystem
│   └── task_main.c
├── build-launchpad
├── build-obc-v0.3
├── build-obc-v0.4
├── platform-launchpad
│   ├── include
│   |   └── spi.h
│   └── source
│       └── sys_main.c
│       └── spi.c
├── platform-obc-v0.3
├── platform-obc-v0.4

platform-obc-c0.x

The platform directory is where HALCoGEN-generated HAL (hardware abstraction layer) code is placed. Additionally, the HALCoGen project file and the FreeRTOS source code are in this directory. The majority of this code is generated for us, and we don't edit it except for very limited circumstances. A lot of the code in this directory is used to configure the MCU based on the settings from HALCoGen. Therefore, there is one platform directory enabled per build configuration.

Include and Source

The platform directory contains include and source subdirectories. If you're looking for a header file that HALCoGen made, such as sci.h, you would find it under include. To see the inner workings of the low-level drivers that HALCoGEN makes, you would go to a .c file in the source subdirectory.

sys_main.c is the entry point generated in HALCoGEN. So when the MCU starts up, it will run the code in this file. Inside sys_main, we initialize hardware peripherals and start the RTOS. You may need to edit this file.

HAL

Other files are known as HAL files, meaning "hardware abstraction layer." These are used to abstract the specifics of the hardware (register settings, addresses, etc.) into easier-to-use functions such as sciSend().

Every time you configure something or change a configuration in HALCoGen, the results make their way into these files. HALCoGEN re-generates ALL of the HAL files every time, so it is important to only make modifications inside the /* USER CODE BEGIN (x) */ and /* USER CODE END (x)*/ comments, or else your modifications will be wiped out next time somebody changes any part of the configuration. Generally, we avoid edits to these files unless they're absolutely necessary, since it's a bit difficult to keep track of.

orcasat

The orcasat directory contains the vast majority of the code we develop. So the application layer (RTOS tasks, various functions, etc.) is here, along with sensor drivers and our custom peripheral drivers like the SPI and I2C drivers. It will be organized into subdirectories for the filesystem, peripheral drivers, and more.

We use the HAL functions from the platform directory in the application layer.

build-obc-v0.x

The build directory is where CCS places all of the .o files and other byproducts of compilation. There is never any reason to go into this directory.

Build Configurations

We use build configurations to set up the build for different hardware targets. Since different board versions have different chips or different physical connections, the HAL and other low level configuration is different. By having a build configuration for each board, the HAL for each version can be set up appropriately, while still enabling our application-level software to work without changes.

Build configurations set the following things:

  • Directories to include in compilation
  • Directories to exclude from compilation
  • The chip part number
  • A #define with the build configuration name
  • The default debug connection

During sfusat 2016-2018 development, there was a build configuration for LaunchPad development, and one for each OBC version. The build configurations are carefully set up to include the correct platform directory, set the appropriate #defines for hardwaredefs, and set the destination of the build to the correct build directory.

You are responsible for setting your build configuration correctly to match the hardware you have, and making sure that when you push code, it will build under each build configuration.

Hardwaredefs

The hardwaredefs file is used to map HALCoGEN-created peripheral names to our own names. This allows us to give a GPIO pin a descriptive name in our code, and then to easily change which port/pin it is connected to. This is useful for supporting different board revisions with #defines, instead of changing things deep in our code.

For example, we may need to connect to the SPI port that supports the radio, and a GPIO pin connected to the blinky debug LED. Depending on the board version, different pins may be used for these features and since our code must run on all hardware versions, we need a way to change these connections around.

The hardwaredefs file may look like this:

// Only set for board version 1 build config
#ifdef BOARD_VERSION_1
#define RF_SPI_PORT       spiREG1
#define blinky_GPIO_PORT  gioPORTA
#define blinky_GPIO_PIN   2

// Only set for board version 2 build config
#ifdef BOARD_VERSION_2
#define RF_SPI_PORT       spiREG5
#define blinky_GPIO_PORT  gioPORTB
#define blinky_GPIO_PIN   7

Our application code would use the names we defined, and they will be corrected back to the appropriate values depending on the build configuration (which will set the BOARD_VERSION_x define).

#include "hardwaredefs.h"
spiSend(RF_SPI_PORT, "hello!");
gioSetVal(blinky_GPIO_PORT, blinky_GPIO_PIN, 1);

Random Troubleshooting

USB Issues

Computer will receive but not transmit to OBC

  • We've seen this happen when the USB cable is plugged into a USB hub. Try without the hub.

Can't send commands to OBC

  • Windows doesn't use standard line endings. Ugh. Set the line endings to \r\n or carriage return and newline in your serial terminal app. The OBC expects these characters to be sent with the enter key to register a command, and they aren't the default on Windows.

Prefetch entry ISR

Sometimes the OBC will start up weird when debugging. Usually the debugger will show you being at the prefetchEntry ISR (although others can happen too). If this happens, mash the system reset and reset buttons in the CCS toolbar a few times. It should reset and bring you to the top of main.

Task Templates

Template for periodic task Template for asynchronous task

These templates would include appropriate watchdog calls to enable correct functionality.


Writing Code - Things to Know

FLIGHT_CONFIG and TEST_CONFIG

Flight and test configurations are used when it is impractical during testing to set a certain parameter as it should be set in flight. An example of this is a task execution frequency. We may only want to log telemetry every 3 minutes in flight, but this low frequency makes testing a task during development impractical since the task would be executed so infrequently.

This is where #FLIGHT_CONFIG and #TEST_CONFIG come in. Whenever you are defining something that should have different flight and testing values, you should set it up using the following snippet:

#ifdef FLIGHT_CONFIG
uint32_t timeout_delay_s = 30000;
#endif

#ifdef TEST_CONFIG
uint32_t timeout_delay_s = 10;
#endif

It's verbose, but will ensure that we don't set up any parameters incorrectly when we flash the flight firmware.

You can set the flight vs. test config using the TODO: environment variable picker screenshot

Built-in types

Use the types that have the bit size in the type name. These can be included with #include 'sys_common.h'.

It's important to use these because the bit size is important in embedded systems. It makes you think about what you're doing and how much memory you actually need. It also helps prevent errors because you will always know the size you're dealing with since it's not obfuscated.

Good:

#include `sys_common.h`
uint32_t foo;
int16_t bar;

Bad and horrible:

int foo;
short bar;

Header Files

Think of header files as the user-facing interface to your API. Therefore, try to only include or declare items in the header file that will be of use outside the context of the .c file.

Functions that will be called all over the place: put in header

Functions that are only used internally: don't put in header

Commenting Style

Comment headers should be used to specify file name, creation date, author(s), description, and major sections within the code

/*******************************************************
 * file_name.c
 * 
 * Created on: Sept 20, 2018 
 *  Author(s): Richard.A + Steven.H 
 * 
 * This section is all about making the best comments
 *
 *******************************************************/

General comments should take the following form:

/**
 * This is a great example of 
 * a multi-line comment.
 */

Inline comments should only be used to clarify exceptionally complex code and should take the following form:

// This is an amazing in-line comment.

Global Variables

Global variables must be declared in the header file using the keyword extern. In the .c file, define or initialize the variable. More info: https://www.cprogramming.com/declare_vs_define.html

To access the variable, simply include the header file with the extern declaration, and refer to the variable by name like usual.

If you don't do this, you may get multiple definition errors or create multiple copies of the same variable.

Header: test.h

extern uint32_t foo;

C file: test.c

uint32_t foo = 234;

Globals should be initialized in an init() function, or they may be initialized at the top of the .c file.

Typical #includes

#include "sys_common.h" - gives you access to the integral types.

Things you Should Never do

Use a function with the suffix noMutex

Functions tagged noMutex are for use inside other functions that are themselves wrapped in a mutex or when mutexes are not available. There are two cases where this may happen:

  • a function executing before the scheduler is started (uncommon)
  • a function within something else that itself takes the mutex (common in low-level code)

HALCoGEN files

https://github.com/SFUSatClub/obc-firmware/wiki/HALCoGEN-Files-we-Modify