10. Developer Instructions

This page goes through how to set up a development environment and use the T3 library for quickly developing and testing your own controllers, filters, estimators, and other algorithms for the T-RECS.

10.1 Introduction

The embedded software installed by default on the T-RECS enables a basic PID controller and moving average filter for the sensor. While this controller and filter are fully capable of running the T-RECS and providing a learning experience for controls courses, one of the more interesting abilities of the T-RECS is to work with many different controller designs as well as filters and/or estimators for the sensor reading. This page details how to develop your own algorithms using the T3 library to quickly facilitate the development and testing.

The T3 library is pre-compiled, meaning that you do not have to worry about the details of how that library works. Moreover, you can develop in any environment that supports the Arduino toolchain and precompiled libraries. For the purposes of these instructions, a Windows development environment based on Visual Studio Code and PlatformIO will be discussed. This development environment is free, easy to use, and well-supported in the community. T3 employees are happy to provide support for any questions with respect to this development environment. For alternative environments, we will support as we can, but you will likely have to spend more time searching forums, etc. to find answers to issues.

10.2 Visual Studio Code Installation

These instructions are for Windows 7 - Windows 11 and have been tested extensively on Windows 10. Go to the following URL (Visual Studio Code download page) and click the download button for the Windows installer.

Once the installer is downloaded, double click on the downloaded installer to run it. We recommend using the default installation options unless you are an advanced user. When you first open Visual Studio Code, it may ask for a default development environment. We will be developing in C/C++, so we recommend selecting C++ to make the environment easier to use.

10.3 PlatformIO Installation

PlatformIO IDE is installed inside of Visual Studio Code, making the integration virtually seamless for users.

1) Click on the extensions icon the left-hand side of Visual Studio Code.

2) Type "platformio" into the search box. The first option that comes up should be the PlatformIO IDE. Click on "PlatformIO IDE"

3) Install PlatformIO IDE by clicking the "Install" button.

It will take some time to download and install. Once completed, you are ready to move on to the next step.

10.4 Git Installation

If you do not already have Git installed, you will need to do so. If you have already installed Git, please skip ahead to 10.5 T3 Library. Go to the Git download URL and click on the Windows button to download Git for Windows.

Once Git is downloaded, double click on the installer to install on your computer. We recommend the default options for installation. Git is easier to use on Windows if you enable the option to integrate it into the right click context menu.

10.5 T3 Library

The T3 Library compiled version is available through github. To make it easy to start developing your own algorithms, we have already set up the library in the format required for PlatformIO with an example main file from which to start.

1) Go to the folder in which you want to work on your project. Right click in the folder and select "Git Bash Here".

2) Action (1) above will open a Bash window in that folder. Next, you need to copy the URL for the T3 Library. Go to the T3 Library link and click on "Code" (1. in the image below). Next, click on the copy icon (2. in the image below). That copies the URL to your clipboard.

3) Go back to the Bash window and type "git clone " at the prompt (be sure to include the trailing space!). Then, right click and select "Paste" from the context menu to paste in the copied URL. The final line should look like the image below.

Press "enter" on your keyboard, and Git does all the work to download the library.

10.6 Open the T3 Library in Visual Studio Code

1) Let's go back to Visual Studio Code. If you have already closed the application, please re-open it. Click on "File" in the upper left-hand corner, then click on "Open Folder" in the menu as shown in the image below.

2) Following the image below, change directories into the EmbeddedProject_Release folder (the folder that was created by your "git clone" action above, then go into the "Libraries" folder (see 1. in the image below). Select "RPiPico" (see 2. in the image below). Only click once on "RPiPico" to select it but not enter the folder. Finally, click on "Select Folder" (shown in 3. in the image below) to open that folder in Visual Studio Code.

3) Editing the main file is available at this point. See 10.7 Editing Your Program for more details. For now, we will point out a couple areas of interest. Once the folder is open, your Visual studio Code explorer should look similar to the image below. In the red circle below, you can see main.ino. This is the main file that you will be editing or replacing as you design your controllers, estimators, filters, etc. Feel free to replace this file, but only have one .ino file in the "src" folder since PlatformIO uses that file as the main file. In the blue circle, you see t3Library.h. This file shows you the available programming interface for the T3 Library. It is intentionally limited to help you focus on developing applications instead of worrying about how the back end systems functions. If you are interested in more details on how the embedded system functions, feel free to ask in our Discord chat, and we will happily provide references for you to learn!

4) Building the program is likewise very simple with PlatformIO since it does all the work for you. First, open the PlatformIO interface as shown below. Click on the icon in the blue circle in the image below. That opens the PlatformIO interface. Then, click on the icon in the red circle below to build. You may have to expand the menu by clicking the arrow next to the "pico" label in order to see the "Build" option. In order to build, PlatformIO will need to download the tool chain, platform environment, etc. Luckily for you, that is all done in the background, so you don't have to worry about it! Just make sure you have an internet connection, and this will all be seamless.

Once the program is built (see the SUCCESS image below), you can navigate to the folder shown in the image below (1. red circle) to get the firmware.uf2 file (2. blue circle) and load that onto your Raspberry Pi Pico, following the instructions in 10.8 Loading Firmware.

10.7 Editing Your Program

In this section, we will walk through several of the key lines in the main.ino file, what their purposes are, and how you can edit the file to develop your own estimators, filters, and controllers.

#include <t3Library.h>
#include <algorithm>
#include <Arduino.h>

The lines above include the necessary library headers for the main file to function. The first include (t3Library.h) is necessary to use the T3 Library and should always be included in the main file if connecting with the T3 computer software. The middle line (algorithm) is used to provide some math functions that have been used in the main file, such as max and min. The final include (Arduino.h) provides access to the Arduino framework that we use for building the embedded code. This include will be especially useful if you use hardware pins in this file to access sensors or actuators directly.

// Set up the TRECS control and sensors
// Controller parameters
#define P_GAIN          (2.3)     // Proportional gain
#define I_GAIN          (4.8)     // Integral gain
#define D_GAIN          (0.4)     // Derivative gain
#define MIN_I_TERM      (-250.0)  // Minimum Contribution of iTerm in PI controller
#define MAX_I_TERM      (250.0)   // Maximum Contribution of iTerm in PI controller

#define MOTOR_CONTROL_PIN (2)

// Sensor parameters
#define MIN_ANGLE_DEG   (-65.0)
#define MAX_ANGLE_DEG   (30.0)
static const uint8_t SENSOR_PIN{26};  // Pin for reading sensor values (ADC0)
#define SENSOR_SLOPE    (-0.3656) // Sensor slope (deg)
#define SENSOR_INTERCEPT (185.64) // Sensor intercept (deg)

These lines define pins and initialization values for the controller and sensor. These lines are not required, since the values can be used without defining variables. However, it is good practice to define values this way to avoid "magic numbers" that have no context.

/* Local controller struct */
typedef struct {
	int8_t  sensor_id;        // ID of the sensor
	int8_t  Ki_id;            // ID of the integral gain
	int8_t  Kp_id;            // ID of the proportional gain
	int8_t  Kd_id;            // ID of the derivative gain
	int8_t  max_i_term_id;    // ID of the maximum integral value
	int8_t  min_i_term_id;    // ID of the minimum integral value
	float   old_error;        // Last error value
	float   integrator_state; // Integrator state (accumulated error)
	uint64_t last_time_ms;    // Last time the controller was called
} PID_t;
PID_t pid_controller;

This controller structure defines the values that are stored and used for the controller. It is not necessary to store the values in a structure; this method was chosen to keep the values grouped together for easy reading and access. Of particular interest are lines 3-8 above. The controller gains are not stored in this structure. Rather, they are stored in the T3 Library so that they can be changed through the front end interface. This structure stores the IDs of those gains, enabling the user to access those gains when needed. If desired, values can be stored directly here. However, those values will not be accessible through the front end interface.

// Sensor
int8_t angle_id;


/* Filter buffer size */
const uint8_t FILTER_BUFFER_SIZE = 3;
float filterBuffer[FILTER_BUFFER_SIZE];  // Array for moving average filter
float filteredVal;                		 // Current filtered valued
int filterIndex;                         // Current index of filterBuffer

The values above are for the sensor and filter. Line 2 is the sensor index, used the same as the gain indices in the controller block in order to access sensor information from the T3 Library. The remaining lines are used for storing the moving average filter for filtering out noisy data.

float filter_sensor(float sensedValue, uint64_t time_ms, bool reset) {
	// Handle if the filter is being reset
	uint8_t max_steps = (reset == true) ? FILTER_BUFFER_SIZE : 1;
	for(uint8_t counter = 0; counter < max_steps; counter++) {
		// Remove oldest value from moving average
		filteredVal -= filterBuffer[filterIndex] / (float)FILTER_BUFFER_SIZE;

		// Add new value to buffer and incrememnt index
		filterBuffer[filterIndex++] = sensedValue;

		// Add new value to moving average
		filteredVal += sensedValue / (float)FILTER_BUFFER_SIZE;

		// Prevent index out of bounds errors
		filterIndex %= FILTER_BUFFER_SIZE;
	}

	return filteredVal;
}

This is a simple moving average filter that retains the last 3 values (per the defines above) and returns an average of those values. The inputs to this function are the current sensed value, the current time in milliseconds, and a boolean defining whether this call is to reset the filter. The reset boolean is primarily used to enable clearing any stored data (e.g. the moving filter data) if called. If this function is replaced by a different filter or an estimator, the function signature (line 1 above) must remain identical (with the exception of the function name) to what is shown above. That is the signature that the T3 Library expects and should provide all necessary data for filtering and/or estimating.

uint16_t updatePID(float goalValue, uint64_t time_ms, bool reset) {

The line above is the signature for the controller function. As was the case for the filter, the signature, with the exception of the function name, must remain identical to the above for use with the T3 Library. This function accepts the goal value (for the T-RECS, this is the target angle), the time in milliseconds, and whether to reset the controller. The reset command is, again, used to notify the user to reset state/retained data. The function returns a value between 0 and 1000. This value is scaled to a PWM command going to the motor (0 is the lowest command, 1000 is max).

	// Get the sensed value
	float sensedValue = 0.0;
	t3Library.getFilteredSensorById(angle_id, &sensedValue);

The lines above are how the sensor values are accessed, using the stored ID. As long as the sensor ID is defined, this function should always return a valid value. To be additionally safe, this function can return a boolean that tells the user whether the returned value is valid. See t3Library.h for the full function definition and documentation.

	// Get controller terms
	float ki = 0.0;
	float kp = 0.0;
	float kd = 0.0;
	float max_i_term = 0.0;
	float min_i_term = 0.0;
	t3Library.getParameterById(pid_controller.Ki_id, &ki);
	t3Library.getParameterById(pid_controller.Kp_id, &kp);
	t3Library.getParameterById(pid_controller.Kd_id, &kd);
	t3Library.getParameterById(pid_controller.max_i_term_id, &max_i_term);
	t3Library.getParameterById(pid_controller.min_i_term_id, &min_i_term);

Like with the sensor values, the lines above are used to get access to the controller parameter values that are stored in the T3 Library and modified through the front end interface. These functions can also return a validity boolean. Please see t3Library.h for the full interface definition.

	// Add PID terms to get new drive signal (0-MAX_PWM scale)
	return uint16_t(std::max(0.0f, pTerm + iTerm + dTerm));  // Do not allow negatives to avoid any wrapping issues

The final line of this function includes a std::max call to ensure that negative values are not returned. The reason is that the conversion to a uint16 (unsigned short) could have issues with negative values resulting in unexpected values. This std::max call ensures that the return value is always what is expected.

void setup() {
	// Local setup
	pid_controller.last_time_ms = 0;

The setup function is run once when the embedded program starts. Line 3 is setting up any values that we use locally in this main.ino function that may not have been defined/set up when the values were defined above. Values inside a structure are good examples since structues do not have a constructor defined, so the values have to be initialized somewhere.

	// Define the command that is driven and that is modifiable through the front end.
	t3Library.defineCommand(0.0, MIN_ANGLE_DEG, MAX_ANGLE_DEG, "Angle(deg)");

The lines above define the command that the user will see in the front end interface. For the T-RECS, that is an angle command. This line should stay the same for any use with the T-RECS.

    // Define controller parameters that are modifiable through the front end.
	// Define default values if the parameters are not already stored in the EEPROM.
	pid_controller.Kp_id = t3Library.addParameter(P_GAIN, true, 0.0, false, 0.0, "Kp");
	pid_controller.Ki_id = t3Library.addParameter(I_GAIN, true, 0.0, false, 0.0, "Ki");
	pid_controller.Kd_id = t3Library.addParameter(D_GAIN, true, 0.0, false, 0.0, "Kd");
	pid_controller.min_i_term_id = t3Library.addParameter(MIN_I_TERM, false, 0.0, false, 0.0, "Min I");
	pid_controller.max_i_term_id = t3Library.addParameter(MAX_I_TERM, false, 0.0, false, 0.0, "Max I");

These lines add the controller parameters (gains and other values) to the T3 Library for management by the library and the front end user interface. See T3library.h for the detailed function documentation.

	angle_id = t3Library.addSensor(SENSOR_PIN, true, MIN_ANGLE_DEG, MAX_ANGLE_DEG, true, filter_sensor, "Angle(deg)");
	t3Library.setSensorLinearModel(angle_id, SENSOR_INTERCEPT, SENSOR_SLOPE);

These lines add the angle sensor (the potentiometer) to the T3 Library management and define how the angle sensor is read (it's a linear model with a default intercept and slope that will be overridden based on the system ID).

	t3Library.setControllerFunction(updatePID);
	t3Library.setMotorControlPin(MOTOR_CONTROL_PIN);

These lines tell the T3 Library about the controller function (updatePID) and define the output pin that will be used for controlling the motor.

        t3Library.setup();

Finally, the internal setup function is called/run. This must be run after all setup information has been defined since this function uses the information defined previously.

void loop() {
	// Update all internal/non-modifiable functions
	t3Library.update();

	// Any updates here should be kept to a minimum because it may affect the timing of the sensors and controller
}

The loop function here enables the T3 Library to update and manage the timing of the system. Because the T3 Library manages the timing, any custom code here should be minimized. Definitely, do NOT put any delays or other calls here that take time since that will break the T3 Library's ability to control the timing of the sensor and controller routines.

One final note: there is a function defined in t3Library.h as shown below:

void setVersion(uint8_t version);

Whenever you change data that is stored in the EEPROM (for example, controller parameters, sensor information, etc.), you should increment this version number. That enables the embedded code to know that something has changed in the EEPROM and to reload the defaults. The maximum value for this version number is 255. Once you hit 255, increment to 0 and start over again.

10.8 Loading Firmware

1) Plug in your T-RECS to your computer via the included USB Micro cable.

2) Press and hold the "BOOTSEL" button on the Raspberry Pi Pico (The blue 1. circle in the image below)

3) Click the "SW1" button on the Raspberry Pi Pico adapter board (the red 2 circle in the image above). Release the BOOTSEL button after clicking the SW1 button.

4) You will now see a new USB drive appear on your computer.

5) Open that USB drive in a file browser as shown in the image below.

6) Drag the "firmware.uf2" file that you located previously onto that drive in the file browser.

7) The file browser will close, and you should see the green light start blinking at 1 second intervals on your Raspberry Pi Pico (the orange 3 circle in the image above).

Congratulations! You have loaded a new program onto the T-RECS!

Last updated