Skip to content

API Reference

Cassandra "ZZ Cat" Robinson edited this page Feb 21, 2024 · 17 revisions

Overview

By design, the API is meant to be simplistic & easy to integrate into your existing sketches/projects.
Code examples are provided as 'pseudo-code' to help convey the context of a particular function and how it should be used

Resource management

Resource management is provided here to explain how the resources (IE memory and hardware) used by CRSF for Arduino can be dynamically allocated as-and-when needed, in order to prevent memory and hardware leaks. Emphasis is placed on memory leaks in particular, because of how vulnerable embedded systems are to it, and because memory leaks are normalised in the maker space. Especially with those that are using the Arduino ecosystem.

You will see references to keywords such as new and delete, plus references to CRSF for Arduino being globally declared as a null pointer, and being initialised only when needed. This is in stark contrast to how Arduino teaches you how to manage objects.

To convey the point (using CRSF for Arduino in the examples) here, this is what Arduino teaches you:

/* Add the class' header to my "sketch". */
#include "CRSFforArduino.h"

/* Global declaration and initialisation of a class. */
CRSFforArduino crsf = CRSFforArduino();

void setup()
{
    /* Initialise the class. */
    crsf.begin();
}

void loop()
{
    /* Run the class' main function. */
    crsf.update();
}

So, what if your firmware is finished using the class object?
You would do something like this in your loop(), right?

void loop()
{
    /* Simulate the class object's main function running for 15 seconds, and then ending itself. */
    static bool crsfEnded = false;

    if (crsfEnded == false)
    {
        static uint32_t start = millis();
        uint32_t now = millis();

        /* Run the main function for 15 seconds. */
        if (now - start <= 15000)
        {
            crsf.update();
        }
        else
        {
            /* The class has finished running.
            It's time to end it. */
            crsf.end();

            /* Set the flag to tell the rest
            of the sketch that we are done here. */
            crsfEnded = true;
        }
    }
}

While this is "simple" and "easy-to-read", it is bad practice.
What you have essentially done here is you have created a memory leak.
In the context of embedded systems (which often has very small flash memory footprints), memory leaks MUST be avoided.
This is because a memory leak eventually consumes all available memory, and a system failure will result from the next call to allocate memory that simply does not exist. IE There is not enough memory available to allocate to something, resulting in the target micro-controller crashing or "locking up".

You do the maths about what happens if your embedded micro-controller is in say, for example, your home-brewed flight controller that you have put in one of your prized expensive helicopters, and that memory leak causes the system failure that I have described here.

Allow me to show you the light among the darkness.
A better way to manage resources is to, first off, declare your class objects as null pointers, like so...

/* Add the class' header to my "sketch". */
#include "CRSFforArduino.h"

/* Global class declaration as a null pointer. */
CRSFforArduino *crsf = nullptr;

This means no resources (IE memory and hardware) will be allocated until you tell it to do so.
This also means that, when resources are allocated, none of those resources will be freed until you tell your firmware to do so.
Fortunately, I have taken great care with respect to how CRSF for Arduino manages resources.
In the example below, I show you how to allocate memory and hardware, plus how to free those resources up when your firmware is finished using CRSF for Arduino...

void setup()
{
    /* Allocate the required memory for the class. */
    crsf = new CRSFforArduino();

    /* Allocate the required hardware of the class. */
    if (crsf->begin() == true)
    {
        /* Hardware allocated successfully.
        The rest of the class' functionality may be initialised here. */
    }
    else
    {
        /* Hardware allocation failed.
        Anything that succeeded during the call to 'begin()' MUST be
        freed to avoid hardware and memory leaks. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard the class' API functions from being accessed
    if the resources allocated were freed. */
    if (crsf != nullptr)
    {
        static uint32_t start = millis();
        uint32_t now = millis();

        /* Run the class' main function for 15 seconds. */
        if (now - start <= 15000)
        {
            crsf->update();
        }
        else
        {
            /* Timeout expired.
            All resources that were allocated MUST be freed,
            starting with freeing up the hardware. */
            crsf->end();

            /* Free up the memory that was allocated. */
            delete crsf;
            crsf = nullptr;
        }
    }
}

The API

Constructors & instantiation

⚠️ ATTENTION ⚠️
You MUST NOT use any of CRSF for Arduino's API functions before instantiation.
You MUST create an instance of CRSF for Arduino before you can use its API.

Constructors

  • CRSFforArduino()

This is the default constructor. It creates an instance of CRSF for Arduino.
Serial1 from the Arduino API is used to provide communications to & from your TBS Crossfire or ExpressLRS receiver.
In the case of STM32 targets, Serial1 is not available on some STM32 targets.
Therefore, the next available HardwareSerial instance is automatically picked based on availability. EG CRSF for Arduino will default to Serial2 if it's available. If Serial2 is not available, Serial3 is automatically picked.

  • CRSFforArduino(HardwareSerial *serialPort)

This constructor is where you can provide your own custom HardwareSerial instance.
This gives you the flexibility of assigning any arbitrary UART port to CRSF for Arduino.

Instantiation

In the example below, CRSF for Arduino will use the default HardwareSerial instance.
In most cases, this is Serial1.

#include "CRSFforArduino.h"

/* Declare a null pointer to CRSF for Arduino. */
CRSFforArduino *crsf = nullptr;

void setup()
{
    /* Instantiate CRSF for Arduino. */
    crsf = new CRSFforArduino();
}

void loop()
{
    /* Your main code here... */
}

Initialising & de-initialising CRSF for Arduino

This is where you actually start using CRSF for Arduino.

Initialiser

  • CRSFforArduino::begin()
  • Return type: bool
    • true if initialisation is successful.
    • false if initialisation failed.

NB: The appropriate baud rate and configuration is done for you.

The example below demonstrates how to initialise CRSF for Arduino.
NB: The return check on the initialiser has been omitted for simplicity's sake.

#include "CRSFforArduino.h"

/* Declare a null pointer to CRSF for Arduino. */
CRSFforArduino *crsf = nullptr;

void setup()
{
    /* Instantiate CRSF for Arduino. */
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino. */
    crsf->begin();
}

void loop()
{
    /* Your main code here... */
}

De-initialiser

  • CRSFforArduino::end()

This de-initialises CRSF for Arduino by disabling hardware serial communications with your RC receiver, & freeing up the previously configured hardware serial port.

⚠️ ATTENTION ⚠️
This does not delete any previously allocated memory for CRSF for Arduino.
You MUST delete this memory yourself using delete <YOUR_CRSF_OBJECT>, where <YOUR_CRSF_OBJECT> is the name you gave to your instance of CRSF for Arduino. EG If you're following the examples above, the name given is crsf. So delete crsf;

The example below shows how to correctly de-initialise CRSF for Arduino and free up the resources that were allocated to it.
In this example, a limited number of executions are done before CRSF for Arduino is completely destroyed.

#include "CRSFforArduino.h"

int loopExecutions = 0;

CRSFforArduino *crsf = nullptr;

void setup()
{
    crsf = new CRSFforArduino();

    if (crsf->begin() != true)
    {
        /* CRSF for Arduino failed to initialise.
        Clean-up the resources that it previously allocated, and then free up the memory it allocated. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Increment the loop executions counter. */
        loopExecutions++;

        /* After five loop executions, destroy CRSF for Arduino. */
        if (loopExecutions >= 5)
        {
            crsf->end();
            delete crsf;
            crsf = nullptr;
        }
    }
}

Running CRSF for Arduino

CRSF for Arduino uses an event-driven API for reading RC Channels, Link Statistics, and setting Flight Modes.
These events are handled internally and they have their own respective callback functions that you can register in your sketches.

The main function

  • CRSFforArduino::update()

This function must be placed inside your main loop(), or in some other function or service routine that is updated as quickly as possible.
It should be updated at least every 4 milliseconds or faster.

#include "CRSFforArduino.h"

CRSFforArduino *crsf = nullptr;

void setup()
{
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino, and clean up
    any allocated resources if initialisation fails. */
    if (crsf->begin() != true)
    {
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Call CRSF for Arduino's main function here. */
        crsf->update();
    }
}

Events - Link Statistics

This is your first port of call, because it gives you insight as to the stability of your receiver's connection with your transmitter.
Here, you have access to RSSI, Link Quality, Signal-to-Noise Ratio, and Transmitter Power.

  • CRSFforArduino::setLinkStatisticsCallback(void (*callback)(serialReceiverLayer::link_statistics_t linkStatistics))
#include "CRSFforArduino.h"

CRSFforArduino *crsf = nullptr;

void onLinkStatisticsUpdate(serialReceiverLayer::link_statistics_t);

void setup()
{
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino. */
    if (crsf->begin() == true)
    {
        /* CRSF for Arduino initialised successfully.
        We can now register the Link Statistics event. */
        crsf->setLinkStatisticsCallback(onLinkStatisticsUpdate);
    }
    else
    {
        /* Clean up any resources,
        if initialisation fails. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Call CRSF for Arduino's main function here. */
        crsf->update();
    }
}

void onLinkStatisticsUpdate(serialReceiverLayer::link_statistics_t linkStatistics)
{
    /* This is your Link Statistics Event Callback.
    By using the linkStatistics parameter that's passed in,
    you have access to the following:
    - linkStatistics.rssi
    - linkStatistics.lqi
    - linkStatistics.snr
    - linkStatistics.tx_power

    For the purposes of this example, these values are simply
    printed to the Serial Monitor at a rate of 5 Hz. */

    static unsigned long lastPrint = 0;
    if (millis() - lastPrint >= 200)
    {
        lastPrint = millis();
        Serial.print("Link Statistics: ");
        Serial.print("RSSI: ");
        Serial.print(linkStatistics.rssi);
        Serial.print(", Link Quality: ");
        Serial.print(linkStatistics.lqi);
        Serial.print(", Signal-to-Noise Ratio: ");
        Serial.print(linkStatistics.snr);
        Serial.print(", Transmitter Power: ");
        Serial.println(linkStatistics.tx_power);
    }
}

Events - RC Channels

Setting up RC channels is done by telling CRSF for Arduino what callback handler you are using.
There are two ways to implement RC channels in your projects:

  1. Process your RC channels in total isolation - IE Within the context of your desired RC Channels Callback Handler.
  2. Copy the parameter passed in by the callback to a globally declared instance of serialReceiverLayer::rcChannels_t.

You decide which method is best for your application. The examples provided in this section will walk you through both implementation techniques.

  • CRSFforArduino::setRcChannelsCallback(void (*callback)(serialReceiverLayer::rcChannels_t *rcChannels))

NB: The RC Channels values themselves are raw values. IE These are the values that have come directly off of your receiver.
If you need to convert these values over to "microseconds", you SHOULD use CRSFforArduino::rcToUs(uint16_t rc).

#include "CRSFforArduino.h"

CRSFforArduino *crsf = nullptr;

/* A flag to hold the fail-safe status. */
bool isFailsafeActive = false;

/* RC Channels data. */
int rcChannelCount = 8;
const char *rcChannelNames[] = {
    "A",
    "E",
    "T",
    "R",
    "Aux1",
    "Aux2",
    "Aux3",
    "Aux4",

    "Aux5",
    "Aux6",
    "Aux7",
    "Aux8",
    "Aux9",
    "Aux10",
    "Aux11",
    "Aux12"};

/* RC Channels Event Callback. */
void onReceiveRcChannels(serialReceiverLayer::rcChannels_t *rcData);

void setup()
{
    crsf = new CRSFforArduino();

    /* Initialise CRSF for Arduino. */
    if (crsf->begin() == true)
    {
        /* CRSF for Arduino initialised successfully.
        We can now register the RC Channels event. */
        crsf->setRcChannelsCallback(onReceiveRcChannels);

        /* Constrain the RC Channels Count to the maximum number
        of channels that are specified by The Crossfire Protocol.*/
        rcChannelCount = rcChannelCount > crsfProtocol::RC_CHANNEL_COUNT ? crsfProtocol::RC_CHANNEL_COUNT : rcChannelCount;
    }
    else
    {
        /* Clean up any resources,
        if initialisation fails. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API with a null check. */
    if (crsf != nullptr)
    {
        /* Call CRSF for Arduino's main function here. */
        crsf->update();
    }
}

void onReceiveRcChannels(serialReceiverLayer::rcChannels_t *rcData)
{
    /* This is your RC Channels Event Callback.
    Here, you have access to all 16 11-bit channels,
    plus an additional "failsafe" flag that tells you whether-or-not
    your receiver is connected to your controller's transmitter module.

    Using the rcData parameter that was passed in,
    you have access to the following:

    - failsafe - A boolean flag indicating the "Fail-safe" status.
    - value[16] - An array consisting of all 16 received RC channels.
      NB: RC Channels are RAW values and are NOT in "microseconds" units.

    For the purposes of this example, the fail-safe flag is used to centre
    all channels except for Channel 5 (AKA Aux1). Aux1 is set to the
    "Disarmed" position.
    The RC Channels themselves are all converted to "microseconds" for
    visualisation purposes, and printed to the Serial Monitor at a rate
    of 100 Hz. */

    if (rcData->failsafe)
    {
        if (!isFailsafeActive)
        {
            isFailsafeActive = true;

            /* Centre all RC Channels, except for Channel 5 (Aux1). */
            for (int i = 0; i < rcChannelCount; i++)
            {
                if (i != 4)
                {
                    rcData->value[i] = 992;
                }
            }

            /* Set Channel 5 (Aux1) to its minimum value. */
            rcData->value[4] = 191;

            Serial.println("[Sketch | WARN]: Failsafe detected.");
        }
    }
    else
    {
        /* Set the failsafe status to false. */
        if (isFailsafeActive)
        {
            isFailsafeActive = false;
            Serial.println("[Sketch | INFO]: Failsafe cleared.");
        }
    }

    /* Here is where you may write your RC channels implementation.
    For this example, RC Channels are simply sent to the Serial Monitor. */
    static uint32_t lastPrint = millis();

    if (millis() - lastPrint >= 10)
    {
        lastPrint = millis();

        Serial.print("[Sketch | INFO]: RC Channels: <");
        for (int i = 0; i < rcChannelCount; i++)
        {
            Serial.print(rcChannelNames[i]);
            Serial.print(": ");
            Serial.print(crsf->rcToUs(rcData->value[i]));

            if (i < (rcChannelCount - 1))
            {
                Serial.print(", ");
            }
        }
        Serial.println(">");
    }
}

Events - Flight Modes

Flight Modes are divided up into two subsections:

  1. Standard modes These are based on the Flight Modes used by Betaflight 4.3.
  2. Custom modes You assign a text-based string to your desired flight mode.

In either case, the API functions are universal, and are as follows:

  • CRSFforArduino::setFlightMode(serialReceiverLayer::flightModeId_t flightModeId, const char *flightModeName, uint8_t channel, uint16_t min, uint16_t max)
    • Use this to set your desired Flight Mode, assign the Flight Mode to your desired RC Channel, and provide the minimum and maximum values in which the Flight Mode will be active.
    • Parameters:
      • flightModeId
        • This is the index ID of your chosen Flight Mode.
      • flightModeName
        • This is where you provide your Flight Mode with a name. It is particularly useful with Custom Flight Modes.
      • channel
        • The channel number you are assigning your flight mode to. When this channel is between the min and max values, this Flight Mode is active.
      • min
        • The minimum channel value (in microseconds) that the Flight Mode will be activated.
      • max
        • The maximum channel value (in microseconds) that the Flight Mode will be activated.
    • Return type: bool
      • true if the Flight Mode was set.
      • false otherwise. Additionally, if CRSF_FLIGHTMODES_ENABLED in CFA_Config.hpp is 0, setFlightMode() will always return false.
  • CRSFforArduino::setFlightModeCallback(void (*callback)(serialReceiverLayer::flightModeId_t flightMode))
    • This is where you set the callback handler for the Flight Modes Event.
    • Parameter:
      • flightMode
        • This is automatically passed in, and you can read from this inside the Flight Modes Event Callback to determine what Flight Mode is active.
Standard Flight Modes
#include "CRSFforArduino.hpp"

#define FLIGHT_MODE_ARM_CHANNEL 5
#define FLIGHT_MODE_ARM_MIN     1000
#define FLIGHT_MODE_ARM_MAX     1800

#define FLIGHT_MODE_ACRO_CHANNEL 6
#define FLIGHT_MODE_ACRO_MIN     750
#define FLIGHT_MODE_ACRO_MAX     1250

#define FLIGHT_MODE_ANGLE_CHANNEL 6
#define FLIGHT_MODE_ANGLE_MIN     1250
#define FLIGHT_MODE_ANGLE_MAX     1750

#define FLIGHT_MODE_HORIZON_CHANNEL 6
#define FLIGHT_MODE_HORIZON_MIN     1750
#define FLIGHT_MODE_HORIZON_MAX     2250

CRSFforArduino *crsf = nullptr;

/* Declare the onFlightModeUpdate callback function. */
void onFlightModeUpdate(serialReceiverLayer::flightModeId_t);

void setup()
{
    /* Initialise CRSF for Arduino. */
    crsf = new CRSFforArduino();
    if (crsf->begin() != false)
    {
        /* CRSF for Arduino initialised successfully.
        Now, the Flight Modes can be configured. */
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_DISARMED, FLIGHT_MODE_ARM_CHANNEL, FLIGHT_MODE_ARM_MIN, FLIGHT_MODE_ARM_MAX);
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_ACRO, FLIGHT_MODE_ACRO_CHANNEL, FLIGHT_MODE_ACRO_MIN, FLIGHT_MODE_ACRO_MAX);
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_ANGLE, FLIGHT_MODE_ANGLE_CHANNEL, FLIGHT_MODE_ANGLE_MIN, FLIGHT_MODE_ANGLE_MAX);
        crsf->setFlightMode(serialReceiverLayer::FLIGHT_MODE_HORIZON, FLIGHT_MODE_HORIZON_CHANNEL, FLIGHT_MODE_HORIZON_MIN, FLIGHT_MODE_HORIZON_MAX);

        crsf->setFlightModeCallback(onFlightModeUpdate);
    }
    else
    {
        /* CRSF for Arduino failed to initialise. */
        crsf->end();
        delete crsf;
        crsf = nullptr;
    }
}

void loop()
{
    /* Guard CRSF for Arduino's API functions. */
    if (crsf != nullptr)
    {
        /* Run CRSF for Arduino's main function. */
        crsf->update();
    }
}

void onFlightModeUpdate(serialReceiverLayer::flightModeId_t flightMode)
{
    /* This is where you would normally write your own Flight Modes implementation.
    For the purposes of this example, the current Flight Mode is printed to the Serial Monitor. */

    /* Are we disarmed? */
    bool isDisarmed = true;
    if (flightMode != serialReceiverLayer::FLIGHT_MODE_DISARMED)
    {
        isDisarmed = false;
    }
    else if (isFailsafeActive)
    {
        isDisarmed = true;
    }

    /* Update the flight mode telemetry with the new value. */
    crsf->telemetryWriteFlightMode(flightMode, isDisarmed);

    /* Print the flight mode to the Serial Monitor, but only if the flight mode has changed. */
    static serialReceiverLayer::flightModeId_t lastFlightMode = serialReceiverLayer::FLIGHT_MODE_DISARMED;
    if (flightMode != lastFlightMode)
    {
        lastFlightMode = flightMode;

        Serial.print("[Sketch | INFO]: Flight Mode: ");
        switch (flightMode)
        {
            case serialReceiverLayer::FLIGHT_MODE_DISARMED:
                Serial.println("Disarmed");
                break;
            case serialReceiverLayer::FLIGHT_MODE_ACRO:
                Serial.println("Acro");
                break;
            case serialReceiverLayer::FLIGHT_MODE_WAIT:
                Serial.println("Wait for GPS Lock");
                break;
            case serialReceiverLayer::FLIGHT_MODE_FAILSAFE:
                Serial.println("Failsafe");
                break;
            case serialReceiverLayer::FLIGHT_MODE_GPS_RESCUE:
                Serial.println("GPS Rescue");
                break;
            case serialReceiverLayer::FLIGHT_MODE_PASSTHROUGH:
                Serial.println("Passthrough");
                break;
            case serialReceiverLayer::FLIGHT_MODE_ANGLE:
                Serial.println("Angle");
                break;
            case serialReceiverLayer::FLIGHT_MODE_HORIZON:
                Serial.println("Horizon");
                break;
            case serialReceiverLayer::FLIGHT_MODE_AIRMODE:
                Serial.println("Airmode");
                break;
            default:
                Serial.println("Unknown");
                break;
        }
    }
}
Custom Flight Modes
Clone this wiki locally