bluetooth Connecting Real World Sensors


Example

For BLE slave devices to do any useful work, the GPIOs of the wireless MCU are almost always involved. For instance, to read temperature from an external sensor, the ADC functionality of GPIO pins may be required. TI's CC2640 MCU features a maximum of 31 GPIOs, given different packaging types.

In the hardware side, CC2640 provides a rich set of peripheral functionalities such as ADC, UARTS, SPI, SSI, I2C etc. In the software side, TI's BLE stack tries to offer a uniform device-independent driver interface for different peripherals. A uniform driver interface may improve the chance of code re-usability, but on the other hand, it also increases the slope of the learning curve. In this note, we use the SPI controller as an example and show how to integrate the software driver into user applications.

Basic SPI Driver Flow

In TI's BLE stack, a peripheral driver often consists of three parts: a device independent specification of the driver APIs; a device specific implementation of the driver APIs and a mapping of hardware resource.

For the SPI controller, its driver implementation involves three files:

  • <ti/drivers/SPI.h> -- this is the device-independent API specification
  • <ti/drivers/spi/SPICC26XXDMA.h> -- this is the CC2640-specific API implementation
  • <ti/drivers/dma/UDMACC26XX.h> -- this is the uDMA driver required by the SPI driver

(Note: the best document for the peripheral drivers of TI's BLE stack can mostly be found at their header files, such as SPICC26XXDMA.h in this case)

To start using the SPI controller, let's first create a custom c file, namely sbp_spi.c, that include the three header files above. The natural next step is to create an instance of the driver and initiate it. The driver instance is encapsulated in the data structure -- SPI_Handle. Another data structure -- SPI_Params is used to specify the key parameters for the SPI controller, such as bit rate, transfer mode, etc.

#include <ti/drivers/SPI.h>
#include <ti/drivers/spi/SPICC26XXDMA.h>
#include <ti/drivers/dma/UDMACC26XX.h>

static void sbp_spiInit();

static SPI_Handle spiHandle;
static SPI_Params spiParams;

void sbp_spiInit(){
    SPI_init();
    SPI_Params_init(&spiParams);
    spiParams.mode                     = SPI_MASTER;
    spiParams.transferMode             = SPI_MODE_CALLBACK;
    spiParams.transferCallbackFxn      = sbp_spiCallback;
    spiParams.bitRate                  = 800000;
    spiParams.frameFormat              = SPI_POL0_PHA0;
    spiHandle = SPI_open(CC2650DK_7ID_SPI0, &spiParams);
}

The above sample code exemplifies how to initialize the SPI_Handle instance. The API SPI_init() has to be called first to initialize internal data structures. The function call SPI_Params_init(&spiParams) sets all fields of SPI_Params structure to default values. Then developers can modify key parameters to suit their specific cases. For example, the above code sets the SPI controller to operate in master mode with a bit rate of 800kbps and uses a non-blocking method to process each transaction, so that when a transaction is completed the callback function sbp_spiCallback will be called.

Finally, a call to the SPI_open() opens the hardware SPI controller and return a handle for later-on SPI transactions. The SPI_open() takes two arguments, the first is the ID of the SPI controller. CC2640 features two hardware SPI controllers on-chip, thus this ID arguments will be either 0 or 1 as defined below. The second argument is the desired parameters for the SPI controller.

/*!
 *  @def    CC2650DK_7ID_SPIName
 *  @brief  Enum of SPI names on the CC2650 dev board
 */
typedef enum CC2650DK_7ID_SPIName {
    CC2650DK_7ID_SPI0 = 0,
    CC2650DK_7ID_SPI1,
    CC2650DK_7ID_SPICOUNT
} CC2650DK_7ID_SPIName;

After successful opening of the SPI_Handle, developers can initiate SPI transactions immediately. Each SPI transaction is described using the data structure -- SPI_Transaction.

/*!
 *  @brief
 *  A ::SPI_Transaction data structure is used with SPI_transfer(). It indicates
 *  how many ::SPI_FrameFormat frames are sent and received from the buffers
 *  pointed to txBuf and rxBuf.
 *  The arg variable is an user-definable argument which gets passed to the
 *  ::SPI_CallbackFxn when the SPI driver is in ::SPI_MODE_CALLBACK.
 */
typedef struct SPI_Transaction {
    /* User input (write-only) fields */
    size_t     count;      /*!< Number of frames for this transaction */
    void      *txBuf;      /*!< void * to a buffer with data to be transmitted */
    void      *rxBuf;      /*!< void * to a buffer to receive data */
    void      *arg;        /*!< Argument to be passed to the callback function */

    /* User output (read-only) fields */
    SPI_Status status;     /*!< Status code set by SPI_transfer */

    /* Driver-use only fields */
} SPI_Transaction;

For example, to start a write transaction on the SPI bus, developers need to prepare a 'txBuf' filled with data to be transmitted and set the 'count' variable to the length of data bytes to be sent. Finally, a call to the SPI_transfer(spiHandle, spiTrans) signals the SPI controller to start the transaction.

static SPI_Transaction spiTrans;
bool sbp_spiTransfer(uint8_t len, uint8_t * txBuf, uint8_t rxBuf, uint8_t * args)
{    
    spiTrans.count = len;
    spiTrans.txBuf = txBuf;
    spiTrans.rxBuf = rxBuf;
    spiTrans.arg   = args;

    return SPI_transfer(spiHandle, &spiTrans);
}

Because SPI is a duplex protocol that both transmitting and receiving happens at the same time, when a write transaction finished, its corresponding response data is already available at the 'rxBuf'.

Since we set the transfer mode to callback mode, whenever a transaction is completed, the registered callback function will be called. This is where we handle the response data or initiate the next transaction. (Note: always remember not to do more than necessary API calls inside a callback function).

void sbp_spiCallback(SPI_Handle handle, SPI_Transaction * transaction){
    uint8_t * args = (uint8_t *)transaction->arg;
    
    // may want to disable the interrupt first
    key = Hwi_disable();    
    if(transaction->status == SPI_TRANSFER_COMPLETED){
        // do something here for successful transaction...
    }
    Hwi_restore(key);
}

I/O Pin Configuration

Till now, it seems reasonably simple to use the SPI driver. But wait, how can connect the software API calls to physical SPI signals? This is done through three data structures: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 and SPI_Config. They are normally instantiated at a different location like 'board.c'.

/* SPI objects */
SPICC26XXDMA_Object spiCC26XXDMAObjects[CC2650DK_7ID_SPICOUNT];

/* SPI configuration structure, describing which pins are to be used */
const SPICC26XXDMA_HWAttrsV1 spiCC26XXDMAHWAttrs[CC2650DK_7ID_SPICOUNT] = {
    {
        .baseAddr           = SSI0_BASE,
        .intNum             = INT_SSI0_COMB,
        .intPriority        = ~0,
        .swiPriority        = 0,
        .powerMngrId        = PowerCC26XX_PERIPH_SSI0,
        .defaultTxBufValue  = 0,
        .rxChannelBitMask   = 1<<UDMA_CHAN_SSI0_RX,
        .txChannelBitMask   = 1<<UDMA_CHAN_SSI0_TX,
        .mosiPin            = ADC_MOSI_0,
        .misoPin            = ADC_MISO_0,
        .clkPin             = ADC_SCK_0,
        .csnPin             = ADC_CSN_0
    },
    {
        .baseAddr           = SSI1_BASE,
        .intNum             = INT_SSI1_COMB,
        .intPriority        = ~0,
        .swiPriority        = 0,
        .powerMngrId        = PowerCC26XX_PERIPH_SSI1,
        .defaultTxBufValue  = 0,
        .rxChannelBitMask   = 1<<UDMA_CHAN_SSI1_RX,
        .txChannelBitMask   = 1<<UDMA_CHAN_SSI1_TX,
        .mosiPin            = ADC_MOSI_1,
        .misoPin            = ADC_MISO_1,
        .clkPin             = ADC_SCK_1,
        .csnPin             = ADC_CSN_1
    }
};

/* SPI configuration structure */
const SPI_Config SPI_config[] = {
    {
         .fxnTablePtr = &SPICC26XXDMA_fxnTable,
         .object      = &spiCC26XXDMAObjects[0],
         .hwAttrs     = &spiCC26XXDMAHWAttrs[0]
    },
    {
         .fxnTablePtr = &SPICC26XXDMA_fxnTable,
         .object      = &spiCC26XXDMAObjects[1],
         .hwAttrs     = &spiCC26XXDMAHWAttrs[1]
    },
    {NULL, NULL, NULL}
};

The SPI_Config array has a separate entry for each hardware SPI controller. Each entry has three fields: fxnTablePtr, object and hwAttrs. The 'fxnTablePtr' is a point table that points to the device-specific implementations of the driver API.

The 'object' keeps track of information like driver state, transfer mode, callback function for the driver. This 'object' is automatically maintained by the driver.

The 'hwAttrs' stores the actual hardware resource mapping data, e.g. the IO pins for the SPI signals, the hardware interruption number, base address of the SPI controller etc. Most fields of the 'hwAttrs' are pre-defined and cannot be modified. Whereas the IO pins of the interface can be freely assigned based user cases. Note: the CC26XX MCUs decouple the IO pins from specific peripheral functionality that any of the IO pins can be assigned to any peripheral function.

Of course the actual IO pins have to be defined first in the 'board.h'.

#define ADC_CSN_1                             IOID_1
#define ADC_SCK_1                             IOID_2
#define ADC_MISO_1                            IOID_3
#define ADC_MOSI_1                            IOID_4
#define ADC_CSN_0                             IOID_5
#define ADC_SCK_0                             IOID_6
#define ADC_MISO_0                            IOID_7
#define ADC_MOSI_0                            IOID_8

As a result, after configuration of hardware resource mapping, developers can finally communicate with external sensor chips through SPI interface.