TACOS

This is the Trajectory Analysis Control OS (TACOS) that serves as a framework for flight computer development. TACOS offers a state machine, a CAN bus interface, a watchdog and other HAL features through it's submodules. It runs on cmsis-rtos2 FreeRTOS on STM32 microcontrollers with C++ (maybe future versions will offer external C interfaces to support a wide array of languages...).

To use TACOS one should implement threads, which fulfill the various roles of the module in the App directory. TACOS utilizes ALPAKA features, in particular requiring sta-core and rtos2-utils, as such it requires these to be in it's include path.

Setting up a TACOS Project

Setting up the Project

First one must create a new CubeIDE project with FreeRTOS. To avoid doing that however we recommend using the ioc-collection to get a preconfigured IOC for the STM microcontroller you are using. From here follow the following steps:

  1. Import -> General -> Import an Existing STM32CubeMX Configuration File (.ioc)
  2. Select the .ioc file from the ioc-collection
  3. Enter the project name and location you want to save the project to
  4. Select C++ as the target language
  5. Click "Finish"

Setting up the Folder Structure

Now it is necessary to setup the dependencies and include paths for TACOS. For this first create a new folder in the project directory called Libs. Then create another folder in the project directory called App with the subfolders Inc and Src. Now also create a folder called sta in the Inc folder. Finally add the empty files App/Inc/sta/config.hpp and App/Src/startup.cpp.

Now your project should look like this:

...
App/
├── Inc/
│   ├── sta/
│   │   └── config.hpp
├── Src/
│   └── startup.cpp
Libs/
...

Setting up the Dependencies

First it is recommended to initialize a git repository in the project folder with git init. Then add the TACOS, sta-core and rtos2-utils repositories as submodules in the Libs folder with the following commands:

cd Libs
git submodule add https://git.intern.spaceteamaachen.de/ALPAKA/TACOS.git
git submodule add https://git.intern.spaceteamaachen.de/ALPAKA/sta-core.git
git submodule add https://git.intern.spaceteamaachen.de/ALPAKA/rtos2-utils.git

Make sure that you add the include paths for TACOS, sta-core and rtos2-utils to the project with the following steps:

  1. Right click your project in the Project Explorer and select Properties -> C/C++ General -> Paths and Symbols -> Includes -> GNU C -> Add...
  2. Select Add to all languages and Is a workspace path
  3. Click on Workspace and select a folder from the YOUR_PROJECT_FOLDER/(Libs|App) directory
    • Always select the include or Inc folder for the include paths
    • If the path you want to add is not in the list, refresh the project with F5 in the Project Explorer and try again
  4. Repeat for TACOS, sta-core, rtos2-utils and the App folder
  5. Right click your project in the Project Explorer and select Properties -> C/C++ General -> Paths and Symbols -> Source Location -> Add Folder...
    • Add the App and Libs folders

Note

You often want to add more submodules during development. Here, a faster way to add the include path for a library is to right click the library's include folder in the Project Explorer and select Add/remove include path.

Starting TACOS

Navigate to the Core/Src/freertos.c file and add the following code to the StartDefaultTask function:

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
	extern void startTACOS(void *);
	startTACOS(argument);

  /* Infinite loop */
  for(;;)
  {
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

This will start the TACOS startup and initialize all TACOS threads (which will then initialize yours).

Configuring TACOS

In order to use TACOS, you need to provide a configuration file in the path sta/config.hpp. The following code is an example for a TACOS-project using default configuration:

#ifndef INC_STA_CONFIG_HPP_
#define INC_STA_CONFIG_HPP_

// Use the STM32F407 microprocessor.
#include <sta/devices/stm32/mcu/STM32F407xx.hpp>

// Enable debug serial output and assertions.
#define STA_ASSERT_ENABLED
#define STA_DEBUGGING_ENABLED

// Enable Features

// Statemachine settings. How many states does your statemachine have?
#define STA_TACOS_NUM_STATES 3

// Uses the default configuration for TACOS.
#include<sta/tacos/configs/default.hpp>

#endif /* INC_STA_CONFIG_HPP_ */

Warning

If you want to use debug printing (enabled using the macro STA_DEBUGGING_ENABLED) in a TACOS project, you should also enable float formatting under Project Settings -> C/C++ Build -> MPU/MCU Settings -> Use float with printf from newlib-nano. This allows you to format print floats using STA_DEBUG_PRINTF. If this setting is not enabled, your software will crash when debug printing floats.

The configuration file shown in the example above initializes the project assuming that you are working on a STM32 Nucleo of type F407. Typically, you are using a different microcontroller, however. In this case you can replace the line

#include <sta/devices/stm32/mcu/STM32F407xx.hpp>

with the include

#include <sta/devices/stm32/mcu/STM32_YOUR_MCU_HERE.hpp>

So far, only a few chips are officially supported. For not officially supported chips use this as the include:

#define STA_STM32_SWD_USART_IDX <IDX OF YOUR UART>

#include <sta/devices/stm32/mcu/common.hpp>
#define STA_MCU_LITTLE_ENDIAN
#define STA_PLATFORM_STM32

Note

The definition of STA_STM32_SWD_USART_IDX allows you to specify which UART handle to use for debug printing. If undefined, a default handle for Nucleos will be used. You can also add the macro STA_STM32_ASEAG instead if you are a cool kid using ASEAG-based hardware 😎.

Warning

The definition of STA_STM32_SWD_USART_IDX has to be placed before the include cpp #include <sta/devices/stm32/mcu/STM32_YOUR_MCU_HERE.hpp>

Warning

Make sure you actually enable the UART bus in the under Pinout & Configuration -> Connectivity in the IOC.

Implementing Your Own Threads

Let's create a simple thread that prints "Hello World" every second. First create a new file in the App/Inc/tasks folder called spam_task.hpp. Then add the following code:

#ifndef INC_TASKS_SPAM_TASK_HPP_
#define INC_TASKS_SPAM_TASK_HPP_

#include <sta/tacos.hpp>

namespace tasks
{
	class SpamTask : public sta::tacos::TacosThread {
	public:
		SpamTask();

    // One time function that is called when the thread is created.
		void init() override;

    // Repeatable function that is called every time the thread is executed.
		void func() override;
	};
} // namespace tasks

#endif /* INC_TASKS_SPAM_TASK_HPP_ */

This code defines a new thread that inherits from TacosThread and implements the init and func functions. The init function is called once when the thread is created and the func function is called every time the thread is executed.

Now create a new file in the App/Src/tasks folder called spam_task.cpp and add the following code:

#include <tasks/spam_task.hpp>
#include <sta/debug/debug.hpp>

namespace tasks {
  SpamTask::SpamTask() :
    TacosThread("SPAM", osPriorityNormal){}

  void SpamTask::init() {
    // Nothing to init...
  }

  void SpamTask::func() {
    // Print "Hello World" every second.
    STA_DEBUG_PRINTLN("Hello World");
    this->periodicDelay(1); // Execute this function with 1 Hz.
  }
} // namespace tasks

Warning

A thread's priority must be strictly lower than the statemachine's priority. Unless manually changed, this is always osPriorityHigh.

To start this thread, we first need to fill out the startup.cpp file. This file may look like this:

#include <sta/tacos.hpp>
#include <tasks/spam_task.hpp>

#include <sta/debug/debug.hpp>

namespace sta
{
	namespace tacos
	{
		void startup()
		{
			// ###### Register different threads for different states here. ######
      		// Register a "Spam Task" thread for all states except 1 and 2.
      		sta::tacos::addThread<tasks::SpamTask>(ALL_STATES - state_set{1,2});

			STA_DEBUG_PRINTF("The answer to everything is %d", 42);
		}
	} // namespace tacos
} // namespace sta

The function startup() is a weakly implemented function that is executed right before TACOS initializes its statemachine task. It serves as an entry point for the user to initialize all busses, threads and rtos2-utils stuff that is needed for the application to fulfill its purpose.

And that's it! Now you have a thread that prints "Hello World" every second. Simply build the project and flash it to your microcontroller and be amazed by the Spam!

[Optional] Setting up the CAN Bus

To enable the CAN Bus two things need to be done:

  1. Enable CAN in the IOC with the RX0 and RX1 Interrupts enabled.
  2. Add the following code to the sta/config.hpp file:
#define STA_TACOS_CAN_BUS_ENABLE

PS: For not officially supported chips add this:

#define STA_STM32_CAN_HANDLE {YOUR_HCAN_HANDLE}

There are two options for handling incoming CAN messages:

  1. If #define STA_CAN_BUS_FWD_ENABLE is set, the messages will be forwarded to the task with the ID of the message.
  • Tasks set their ID with setID(uint32_t id) in their constructor.
  • From here they can handle the message by going through their CAN_queue_ with CanSysMsg msg; CAN_queue_.get(&msg);
  1. All messages will trigger the weakly defined handleSysMessage callback.
  • This could be implemented like this:
namespace sta
{
	namespace tacos
	{
		bool handleSysMessage(CanMsgHeader &header, uint8_t *payload)
		{
      // Print the message ID and the first byte of the payload. 
      //(please don't do this in production, it will crash the system sooner or later)
			STA_DEBUG_PRINTF("> ID: %d", header.sid);

			switch (header.sid)
			{
			// State transition message
			case STA_TACOS_CAN_BUS_SYS_MSG_ID:
				// First byte of payload is the origin state, second byte is the destination state
				tacos::setState(payload[0], payload[1], 0, true);
				return true;

		  case MODULE_SW_RESET_CAN_ID:
        HAL_NVIC_SystemReset();

        return true; // :)

      // ...

		  default:
				return false;
			}

			return false; // I know, i know, this is not necessary, but it's good practice. And you know what they say about good practice: Do it!
		}
	}
}

TACOS Usage Guide

Almost all of the important aspects of working with TACOS have already been discussed when setting up the project itself. The following sections will give you an in-depth explanation of how to use the statemachine, inter-thread communication and the CAN-Bus.

Using the Statemachine

The statemachine forms the heart and soul of a TACOS-based project. Upon initialization, TACOS starts a statemachine that manages the system state and the currently active threads. As seen before, whenever we pass a new thread to TACOS we also have to provide all states in which the thread should run. After each state transition from state x to state y the statemachine task performs two actions:

  1. All threads that should run in state y but are not currently running are started.
  2. All threads that should not run in state y but are currently running are stopped.

Important

The statemachine does not immediately stop a thread and deletes it from memory. Instead, the thread is allowed to finish the current execution of its func before entering a blocked state. This allows the thread to release all its resources.

In order to fully understand the statemachine, we have to take a look at the lockout and failsafe timer. These lockout and failsafe timers are the result of design choices made during early stages of STAHR. The goal was to combine the state estimation (i.e. sensor fusion using a Kalman filter) with timer-based safety mechanisms. For example, our goal was to block any state transition to the state DROGUE before 60 seconds after liftoff. Additionally, a timer was started to automatically switch to state DROGUE after 120 seconds after liftoff.

These safety mechanisms resulted in the implementation of the lockout and failsafe timer in TACOS:

  1. Lockout Timer: The lockout timer can be started after a state transition. As long as it is running, all state transitions are blocked by the statemachines, unless the user actively chooses to bypass the safety mechanism using forceState().
  2. Failsafe Timer The failsafe timer can be used to schedule a state transition after a certain period of time has elapsed. This transition will be blocked if the lockout timer is running at that time. The failsafe timer obeys the following rules:
    • A timed state transition can be requested even when the lockout timer is active. It only matters if the lockout timer is running at the end of the time span of the lockout timer.
    • If a state transition is triggered before the end of the time span, the failsafe timer is stopped.

A state transition can be triggered by calling the functions requestState(), forceState() or setStateTimed() that are provided in sta/tacos.hpp. Take a look at include/sta/README.md for more details on the functions. Additionally, state transitions can be triggered remotely using the CAN-Bus, however, this is discussed in more detail in the section on the CAN Bus.

Example Usage

Generally, the state transitions are requested in the startup() function or in TacosThread instances implemented by the user. It is good practise to give your states names by defining an enum in a header file states.hpp that can be included everywhere in your project.

#ifndef MY_PROJECT_STATES_HPP
#define MY_PROJECT_STATES_HPP

namespace my_project
{
	enum class States : uint16_t
	{
		STARTUP			= 0,
		PING			= 1,
		PONG			= 2
	};
}

#endif // MY_PROJECT_STATES_HPP

This gives us three states: STARTUP, PING and PONG. Generally, these names have no meaning for TACOS but they make your software more readable. Next, we define two modified tasks based on SpamTask for our project:

#include <tasks/ping_task.hpp>
#include <tasks/pong_task.hpp>
#include <path/to/states.hpp>

namespace tasks {
  PingTask::PingTask() :
    TacosThread("PING", osPriorityNormal){}

  void PingTask::func() {
	sleep(100);

    STA_DEBUG_PRINTLN("PING");

	sta::tacos::requestState(my_project::PING, my_project::PONG);
  }

  PongTask::PongTask() :
    TacosThread("PONG", osPriorityNormal){}

  void PongTask::func() {
	sleep(100);

    STA_DEBUG_PRINTLN("PONG");

	sta::tacos::requestState(my_project::PONG, my_project::PING);
  }
} // namespace tasks

Important

Generally, you want both tasks to be implemented in separate .cpp files.

Using these two threads we can implement our startup() function:


#include <sta/tacos.hpp>
#include <tasks/spam_task.hpp>
#include <path/to/states.hpp>

#include <sta/debug/debug.hpp>

namespace sta
{
	namespace tacos
	{
		void startup()
		{
      		// Register a "PingTask" thread for the state PING.
      		sta::tacos::addThread<tasks::PingTask>({my_project::PING});
			sta::tacos::addThread<tasks::PongTask>({my_project::PONG});

			// Start with the spam after one second.
			sta::tacos::setStateTimed(my_project::STARTUP, my_project::PING, 1000);
		}
	} // namespace tacos
} // namespace sta

The resulting program switches between the states PING and PONG and alternately outputs "PING" and "PONG" via UART. While this is just a toy example, building more complicated applications is not much harder!

Thread Priority Design

Every thread running in your TACOS project has a priority that the underlying RTOS uses for scheduling. The list of available priorities can be found here. Contrary to operating systems for your personal computers like Linux or Windows, the scheduling rules in the context of TACOS are rather simple: The scheduler always picks the highest priority thread that is active (i.e. not blocked because it is waiting for an event) and allows it to run. If there are multiple active threads with identical priority, the scheduler uses the round-robin principle.

Warning

If you have a high priority thread that is always active, lower priority threads won't receive any computing time (commonly called starvation). You should ensure that all higher priority threads reach the stated blocked eventually.

We conclude this section with a short list of design principles that have been useful in previous projects:

  • Find a base priority (usually osPriorityNormal) that all threads should have by default. Those are then scheduled via round-robin.
  • There is no reason for your radio thread to check for incoming messages every two milliseconds. Use thread blocking to limit the amount of computing time a thread consumes. For example, this could be the method sleep(ms) that the TacosThread class provides. It's also always a good idea to use interrupts in combination with RtosEvents (discussed later).
  • What are more important tasks in your system? What tasks have strict time requirements? Find a new base priority (for example osPriorityAboveNormal) and assign it to all higher priority threads. Repeat this step if these threads are not equally important.
  • Use global timers in combination with events (as dicussed later in this chapter) to ensure that threads are unblocked at different times.

Warning

The priorities of your threads must always be lower than the priority of the Statemachine in TACOS. Per default, the the statemachine has priority osPriorityHigh.

Using Inter-Thread Communication

This section serves to give you a brief overview of both the fundamental building blocks and useful design patterns for inter-thread communication.

Mutexes

A Mutex is used for managing access to critical sections in your code. A critical section could be code that performs transmissions via a bus or a radio module or code that edits specific memory. Remember that the main benefit (and downside!) of a RTOS is that code is running concurrently or even in parallel. Since there is no guarantee that a thread finishes its operations in a critical section before another thread is given computing time, it is good practise (and often mandatory) that we protect critical sections using mutexes. For example, consider the following scenario in which two threads increment a variable X:

  • Thread A and thread B are running concurrently on your microcontroller.
  • Thread A reads the value 54 fromm the global variable X.
  • Thread B reads the value 54 fromm the global variable X.
  • Thread B writes value 54+1 into global variable X.
  • Thread A writes 54+1 into global variable X. At the end, the global variable contains the value X has the value 55 since the modification by thread B was overwritten. Notice, however, that the result depends on the order in which the two threads are executed. In order to keep the result independent of the order of execution, we use mutexes to grant threads exclusive writing and reading rights to X:
// global.hpp

namespace counter
{
	void increment();
}

// global.cpp
#include <sta/rtos/mutex.hpp>

namespace counter
{
	uint32_t count = 0;
	sta::RtosMutex * mutex = nullptr;

	void increment()
	{
		// Create mutex once when this function is called for the first time.
		if (mutex != nullptr)
			mutex = new sta::RtosMutex("Counter");

		mutex->aquire();
		count++;
		mutex->release();
	}
}

Next, let show how the func of the two threads look like:

// thread_A.cpp

namespace tasks 
{
	// ...

	void ThreadA::func()
	{
		// Do lots of cool stuff

		counter::increment();
	}

	// ...

	void ThreadA::func()
	{
		// Do lots of cool stuff

		counter::increment();
	}

	// ...
}

Before the instruction count++ is performed, the active thread has to aquire the mutex. If the mutex is currently held by another thread, the active thread will be blocked until the mutex is available. This means that the underlying RTOS will stop it and switches to a different thread that is ready. Once the current owner releases the mutex, the thread will enter the ready state again.

Global Timers

Further information

To look into other function of TACOS please consult the READMEs in the include folder or the doxygen documentation. Also consult the sta-core and rtos2-utils READMEs for further information on the features that TACOS uses.

Description
No description provided
Readme GPL-3.0 3.3 MiB
Languages
C++ 98.3%
CMake 1%
C 0.7%