Qualcomm

Getting Started with the Snapdragon Flight: Driver Development

Earlier this year I was tasked with figuring out how to write a custom device driver for the Snapdragon Flight.

While the process ended up being straightforward, documentation and pointers are largely lacking for the Snapdragon Flight environment. What follows is a summary of the information I learned on my driver development journey. I hope to speed up future driver developers by providing a starting point.

If you're just getting started with the Snapdragon Flight, check out this article for development environment setup and useful resources.

Table of Contents

A Brief Overview

Before we dive into the specifics of implementing drivers on the Snapdragon Flight, we need to have a basic understanding of how the application processor (AP) and digital signal processor (DSP) interact.

Communication between the AP and DSP happens through an RPC mechanism. The default mechanism provided in the Hexagon SDK is FastRPC, which utilizes a serial link between the AP and DSP. A Qualcomm IDL compiler is used to generate function stubs for the AP and DSP. The generated functions are implemented on the DSP side and can be called by the AP side. This IDL/RPC mechanism is the way that your application will interact with drivers and other software running on the DSP.

On the Snapdragon Flight, all access to hardware peripherals is limited to the DSP. There is no direct access to peripherals from the AP. In order to talk to a hardware device from the AP, you must write a device driver that will run on the DSP. The AP-side program can utilize the supplied RPC mechanisms to call DSP functions and retrieve data.

Code intended to run on the DSP must be compiled as a shared library (.so). The DSP libraries are found in /usr/share/data/adsp/ by default. Any shared libraries located in this folder will be loaded and executed on the DSP.

The DSP is running Qualcomm's proprietary QuRT RTOS. You can't access the DSP code directly, but Qualcomm provides a DSP abstraction layer (DSPAL) API. Device drivers and other DSP software will utilize the DSPAL as its base layer.

DriverFramework

The simplest way to start developing your device drivers is to use the DriverFramework project, which is based off of the PX4 DriverFramework. DriverFramework is the approach I took for my own device driver development.

DriverFramework is built upon the Hexagon DSPAL and provides a framework for managing multiple device drivers. The framework is compiled into a shared library that runs on the Hexagon DSP. You can define custom functions in a Qualcomm IDL file, as described above. The DSP library must implement the custom IDL functions. A user application running on the AP can call the functions to interact with our custom drivers.

DriverFramework comes with a few device driver examples that can be used as a reference. Some drivers, such as the BMP280, work on the Snapdragon Flight and can be directly used.

DriverFramework Overview

The main framework classes are:

  • Framework
    • Used to start and stop the driver framework
  • DevMgr:
    • Registers and unregisters device drivers
      • gets and releases DevHandle objects
  • WorkMgr:
    • Used by drivers to:
      • schedule periodic tasks
      • create and destroy WorkHandles
  • DevObj:
    • The base class of all drivers
    • Defines the periodic callback method virtual void _measure()

The DriverFramework core consists of one worker thread (class HRTWorkQueue) that periodically executes the method virtual void DevObj::_measure(), that is implemented by the corresponding device driver to update its data.

The DriverFramework supports two methods for interacting with drivers:

  1. Calling member functions with the C++ driver instance
  2. Accessing the device handle (e.g. /dev/iic-0/baro0) and calling POSIX functions (ioctl, read, write)

The device handle enables you to access the driver via a device path from anywhere in the code, without requiring direct access to the driver instance:

DevHandle h;
DevMgr::getHandle("/dev/gyro0", h); // Starts the driver

SomeDataStruct data[3];
int ret = h.read(data, sizeof(data));
if (ret < 0) {
    printf("Error read failed (%d)\n", h.getError());
}

Device Driver Implementation

The framework provides three base driver classes:

  • VirtDevObj: Provides a base class for simulated drivers
  • I2CDevObj: Provides a base class for I2C drivers
  • SPIDevObj: Provides a base class for SPI drivers

If you're implementing a SPI or I2C device, it should inherit from the base classes above. The base classes provide functions which your driver can use to talk over the I2C or SPI bus.

Higher-level sensor classes are also defined inside of the framework:

  • ImuSensor
  • MagSensor
  • BaroSensor

In order to create your device driver, you need to inherit from one of these base classes (or DevObj at a minimum). For example:

#define I2CMUX_CLASS_PATH  "/dev/i2cmux"

class I2CMux : public I2CDevObj
{
public:
    I2CMux(const char *device_path, uint32_t channels, unsigned int sample_interval_usec) :
        I2CDevObj("i2cMux", device_path, I2CMUX_BASE_PATH, sample_interval_usec), max_ch_(channels)
    {}

// etc…
};

Note the I2CMUX_BASE_PATH argument above. This is the base device path that can be used for accessing the device, such as /dev/iic or /dev/i2cmux. Whenever a device is initialized using a specific base path, the first device is created as /dev/i2cmux0. A second driver initialized with the same base path would be created as /dev/i2cmux1, a third as /dev/i2cmux2, etc.

The device_path argument tells us what our parent device path is. For an I2C Mux, our parent might be /dev/iic-0 or /dev/iic-1.

Each driver must also specify a sample_interval_usec argument. This controls the _measure() function periodicity. The _measure() function is a callback that is scheduled for each driver. For example, every 50ms we want to read from our accelerometer. The sample_interval_usec should be specified as 50000 (usec). Any periodic work that needs to be done should happen in the _measure() function, such as reading from the accelerometer, interpreting the result, and adding it to a queue.

In some cases, such as the I2CMux example above, a periodic callback is not needed. Our mux is only interacted with when the mux channel configuration needs to be changed. In that case, simply specify an empty _measure function:

void I2CMux::_measure()
{
    return;
}

Note that the sample_interval_usec cannot be set to 0, so for devices that don't need the periodic callback, just set it to a large interval.

Each driver must also supply a start() and stop() function. Note that each driver is responsible for starting and stopping its parent instance. Using our I2CMux example, we must manually start our I2CDevObj parent:

int I2CMux::start()
{
    int result = I2CDevObj::start();

    if (result != 0)
    {
        DF_LOG_ERR("error: could not start I2C parent: %d", result);
        return result;
    }

    return DevObj::start();
}

Aside from these basic framework requirements, you can implement member functions as you would with any other C++ class. While the _measure() function is called automatically by the framework, you can supply any particular interface you want through the driver object.

Starting and Stopping Device Drivers

By default, the driver is initialized and started the first time a handle (DevHandle) is opened to the device (if it is not running already). It keeps running when the last handle is released.

However, the use of a handle to access the device is optional. The driver can be explicitly started or stopped using start() or stop(). To manually start a driver, make sure to call init() and then start():

myMux.init();
myMux.start();

To stop the driver, simply call stop():

myMux.stop();

Your driver's member functions will fail if you forget to call start() on your driver or fail to start() your parent class.

Using our DSP Device Driver

The test folder inside of the DriverFramework project shows an example framework application. You can use the test/qurt project as a launch point for your own DriverFramework application.

You can define a QURT_BUNDLE to generate the artifacts for an AP/DSP combo:

QURT_BUNDLE(APP_NAME df_testapp
    APPS_SOURCES df_testapp.c
    DSP_SOURCES
        df_testapp_dsp.cpp
        ../test.cpp
    DSP_LINK_LIBS 
        df_driver_framework
        df_framework_test
        ${df_link_libs}
    DSP_INCS ${CMAKE_SOURCE_DIR}/framework/include
    APPS_COMPILER ${ARM-LINUX-GNUEABIHF-GCC}
    )

Any APPS_SOURCES will be compiled into a binary and loaded into /home/linaro by default.

Any DSP_SOURCES will be compiled into a shared library and loaded to /usr/share/data/adsp by default. You can also link in other libraries using DSP_LINK_LIBS, such as the DriverFramework itself (df_driver_framework) and any drivers you might need (e.g. df_i2cmux or df_bmp280).

The QURT_BUNDLE uses the APP_NAME argument to find a matching IDL file (e.g. df_testapp.idl). This IDL file defines the interface between the AP and DSP:

#ifndef DF_TESTAPP_IDL
#define DF_TESTAPP_IDL

#include "AEEStdDef.idl"

interface df_testapp{
    int32 do_test();
};

#endif /*DF_TESTAPP_IDL*/

In the above file, we create a function called do_test(). This function will be prepended with df_testapp, resulting in a final function df_testapp_do_test(). Our DSP code must implement this function:

int32 df_testapp_do_test()
{
    LOG_MSG("Starting df_testapp");

    return doTest();
}

int doTest()
{
    int ret = Framework::initialize();

    if (ret < 0) {
        DF_LOG_ERR("Framework::initialize() failed");
        return ret;
    }

    DFFrameworkTest df;

    bool tests_ok = df.doTests();

    Framework::shutdown();

    return (tests_ok ? 0 : 1);
}

Our DSP code also needs to declare our driver objects and ensure that the framework is initialized. You can statically allocate drivers, but they must be initialized before use.

// J9 connector -> I2C-2
#define I2CMUX_DEVICE_PATH "/dev/iic-2"

// Parent path, addr, channel count
I2CMux mux0(I2CMUX_DEVICE_PATH, 0x70, 8);
I2CMux mux1(I2CMUX_DEVICE_PATH, 0x71, 8);

Our AP side code can call the IDL functions to interact with the DSP:

int main()
{
    printf("Running DF unit test on DSP\n");
    return df_testapp_do_test();
}

We can supply any number of interfaces between the AP and DSP. Just keep in mind that the DSP side is responsible for managing the device drivers, and the AP side can use the IDL functions to control behavior or retrieve data.

DSPAL

The DriverFramework project comes with a operating model that may not make sense for your purposes. The DSPAL APIs provide you with more direct control for building your own single-driver library or custom driver framework.

The DSP Abstraction Layer (DSPAL) provides a standard interface for porting code to the Hexagon processor. Many familiar POSIX APIs are included, such as pthread, timer, semaphore, and signals. The DSPAL also provides hardware abstractions for:

  • GPIO
  • PWM
  • Serial
  • I2C
  • SPI

Loading Files

Remember that our DSP libraries must be loaded to /usr/share/data/adsp/. AP-side programs can be run from anywhere else, location is not particularly important.

The cmake_hexagon project supplies macros to enable file transfers as part of the build process. These provide a *-load build target, which can be run from the CMake build directory. For example:

cd build_qurt 
make df_custom-load

If you want to manually load files, you can use adb:

adb push driver_framework.so /usr/share/data/adsp
adb push df_custom /home/linaro

Helpful Notes

I ran into quite a few problems while implementing my first drivers on the Snapdragon Flight. Here are some important notes to keep in mind.

Sleeping

We often want to call a function to sleep() or delay() when we're interacting with hardware.

For DriverFramework, the correct call is usleep() (implemented in DSPAL). Time is specified in microseconds.

Hexagon SDK Unsupported Software Features

At the time of this writing, the Hexagon SDK supported by the ATLFlight projects is pretty old. C++11 features are nominally supported, but many are missing.

Check this list if you are running into any problems with missing symbols. This list is not complete, but simply contains the functions that caused me problems.

Missing C++ Features:

  • std::tie (not implemented)
  • std::unique_ptr
  • std::shared_ptr
  • tuple (no header)
  • mutex (no header defined)
  • ifstream (missing function dependencies)
  • ofstream (missing function dependencies)
  • stringstream (missing function dependencies)
  • isnan (Dtest not defined)
  • isinf (Dtest not defined)

Missing C Features:

  • fseek (not defined)
  • ftell (not defined)
  • fputc (not defined - stub defined in elisa.cpp to work with JSON parsing)

Helpful IDL Notes

Always use the type int32 for the return type of your IDL functions. Using a boolean caused RPC memory to not correctly be returned from the DSP to the AP.

The in, inrout, and rout types used in the IDL have special meanings:

  • Declaring a buffer as in results the following behavior:
    1. AP flushes the cache for the buffer
    2. AP makes RPC call
    3. DSP invalidates the cache for the buffer before reading it
  • Declaring a buffer as rout results in the following behavior:
    1. AP makes RPC call
    2. DSP flushes the cache after writing to the buffer
    3. AP invalidates the cache for the buffer before reading it
  • Declaring a buffer as inrout results in the following behavior:
    1. AP flushes teh cache for the buffer
    2. AP makes RPC call
    3. DSP invalidates the cache for the buffer before reading it
    4. DSP updates the buffer and flushes the cache
    5. AP invalidates the cache for the buffer before reading it

Further Reading

Getting Started with Snapdragon Flight: Dev Environment Setup & Useful Resources

I recently started working with Qualcomm's Snapdragon Flight kit. The platform looks pretty interesting, but instructions for getting started are scattered around various locations. I've been compiling the disparate instructions and will be sharing simplified guides for anyone else who's annoyed by the unorganized documentation.

First things first: let's walk through the development environment setup.

Setting Up a Linux Snapdragon Development Environment

The instructions below cover the setup process for Snapdragon Flight development using Ubuntu 14.04. I have not tested these instructions on other distributions, though I do not expect the steps to change.

Download SDK Packages

Install the following packages via apt-get (or another package manager):

sudo apt-get install git build-essential cmake default-jre

Then download these two SDK packages:

  • Hexagon SDK: download v3.0 to work with the current GitHub Project
    • You will need to create an account with Qualcomm if you don't already have one
  • qrlSDK: download Flight_3.1.3_qrlSDK.tgz to work with the current GitHub Project
    • You will need to create an account with Intrynsic if you don't already have one

Toolchain Setup

There is a handy cross_toolchain GitHub project which simplifies the installation process for us.

First, clone the repository:

git clone git@github.com:ATLFlight/cross_toolchain.git

Unzip the Hexagon SDK v3.0 package and place the qualcomm_hexagon_sdk_lnx_3_0_eval.bin file in the cross_toolchain/downloads folder.

Move the qrlSDK Flight_3.1.3_qrlSDK.tgz file into the cross_toolchain/downloads folder as well. Unzipping is not required for this file.

Then run the installer:

./installsdk.sh --APQ8074 --arm-gcc --qrlSDK

This will install the following items:

  • Hexagon SDK to ${HOME}/Qualcomm/Hexagon_SDK/3.0
  • Hexagon Tools to ${HOME}/Qualcomm/Hexagon_Tools/7.2.12/Tools
  • ARMv7hf cross compiler: ${HEXAGON_SDK_ROOT}/gcc-linaro-4.9-2014.11-x86_64_arm-linux-gnueabihf_linux
  • qrlSDK to ${HOME}/Qualcomm/qrlinux_sysroot/merged-rootfs

If you re-run the installsdk.sh script, it will only install missing pieces and display environment variables to set. You can also remove packages by omitting options in subsequent runs (like --arm-gcc or --qrlSDK).

You can also create a zipfile of the SDK for archiving and distribution:

/installsdk.sh --APQ8074 --zip --arm-gcc --qrlSDK

A Minimal SDK Installation Profile

If you want to remove all non-essential files (e.g. for setting up a continuous integration server), you can run the following command:

./installsdk.sh --APQ8074 --trim --arm-gcc --qrlSDK

You can create a zipfile of the trimmed SDK for archiving and distribution:

/installsdk.sh --APQ8074 --trim --zip --arm-gcc --qrlSDK

Environment Setup

You'll need to add the following definitions to your .bashrc or .bash_profile files in order for the SDK to work correctly:

export HEXAGON_SDK_ROOT=/home/parallels/Qualcomm/Hexagon_SDK/3.0 
export HEXAGON_TOOLS_ROOT=/home/parallels/Qualcomm/HEXAGON_Tools/7.2.12/Tools 
export HEXAGON_ARM_SYSROOT=/home/parallels/Qualcomm/qrlinux_sysroot 
export ARM_CROSS_GCC_ROOT=/home/parallels/Qualcomm/ARM_Tools/gcc-4.9-2014.11

I also recommend adding the following directory to your PATH so you don't have to remember where mini-dm is found:

${HEXAGON_SDK_ROOT}/tools/debug/mini-dm/Linux_Debug/

Example for adding it to the PATH inside your .bashrc file:

export PATH=$PATH:${HEXAGON_SDK_ROOT}/tools/debug/mini-dm/Linux_Debug

Android SDK Setup

The Snapdragon Flight development environment uses adb to interact with the device.

You will need to download the Android SDK tools from Google.

To install the SDK, unzip the SDK tools and run the following command:

tools/android update sdk --no-ui

You'll want to add the Android tools to your PATH:

/home/parallels/android-sdk-linux/platform-tools (in my case)

With the path changes we had above, your export PATH line should look similar to this:

export PATH=$PATH:${HEXAGON_SDK_ROOT}/tools/debug/mini-dm/Linux_Debug:/home/parallels/android-sdk-linux/platform-tools

Talking to the Snapdragon Flight

Once the Android SDK is installed, connect to the Snapdragon Flight's mini-USB connector. When you run the adb devices command, you should see a device listed:

$ adb devices
List of devices attached
158e2b08    device

You can then connect to the device using adb shell. You should see a prompt for the root user:

$ adb shell
root@linaro-developer:/#

Testing the Environment: Let's Build an Example!

Now that we've set up our environment, let's build the Snapdragon Flight HelloWorld project. This demo project builds for both the application processor and the Hexagon DSP, so we can confirm quickly whether our toolchain setup was successful.

First, clone the HelloWorld project:

git clone git@github.com:ATLFlight/HelloWorld.git

We'll build with the QURT_BUNDLE option, which will produce both the AP and DSP applications.

cd HelloWorld/bundle
make

Once the build has completed, we can send the test app to the device. With the Snapdragon Flight connected over USB, simply run the following command from the bundle/ folder:

make load-bundle

We can connect to the device using the adb shell command. Change to the home directory:

cd /home/linaro

And you should see a helloworld application listed.

When you run the helloworld application, you should see a print statement:

$ ./helloworld
Asking DSP to say hello

We'll need to open another shell to verify the output on the DSP. If you added the mini-dm directory to your PATH, simply invoke mini-dm to open up a DSP debug log instance. If you did not add it to your PATH, invoke the following command:

${HEXAGON_SDK_ROOT}/tools/debug/mini-dm/Linux_Debug/mini-dm

Now that you have a DSP log window up, run the helloworld application again. You should now see a new entry in the DSP log window, similar to this line:

DMSS is connected. Running mini-dm...
[08500/00]  01:31.986  HAP:38:Hello World  0037  helloworld_dsp.c

Our SDK works, and we've successfully loaded a demo application that runs on the AP and DSP!

Useful Links

These have been the most useful resources that I have found so far:

Further Reading