Texas Instruments' (TI) CC26XX series SoCs are readily available wireless MCUs targeting Bluetooth Low Energy (BLE) applications. Along with the MCUs, TI offers a full-fledged software stack that provides necessary API and sample codes to help quickly get developers started with the tool chain. However, for beginners, there is always the question of where to start in front of a long list of reference document and codes. This note aims to record down the necessary steps that it takes to kick the first project going.
The Simple Peripheral Profile is the 'Hello World' example of the BLE stack, where the MCU is acting as a BLE peripheral to upstream hosts, or BLE service clients, like PC and smartphones. Common real world applications include: Bluetooth headphone, Bluetooth temperature sensor, etc.
Before start, we first need to gather basic software and hardware tools for the purpose of programming and debugging.
BLE stack
Download and install TI's BLE-STACK-2-2-0 from the official website. Assume it is installed in the default location 'C:\ti'.
IDE - there are two options:
IAR Embedded Workbench for ARM. This is a commercial tool with a 30 days free evaluation period.
TI's Code Composer Studio (CCS). TI's official IDE and offers free license. In this example we will use CCS V6.1.3
Hardware programming tool
Recommend TI's XDS100 USB-interface JTAG device.
The Simple Peripheral Profile sample code comes with the BLE-Stack installation. Follow the steps below to import this example project to CCS.
The Simple Peripheral Profile example include two projects:
'cc2650em' is the code name for TI's cc2650 evaluation board. The _stack project includes the codes and binary of TI's BEL-Stack-2-2-0, which handles the Bluetooth advertising, handshaking, frequency synchronization etc.. This is the part of code that is relatively stable and don't want to be touched by developers most of the time. The _app project is where developers implement their own tasks and BLE service.
Click on menus 'Project->Build All' to build both projects. If the compiler report some sort of internal error on linking, try disable the 'compress_dwarf' option for the linker by:
After both projects are built successfully, click 'Run->debug' separately to download both the stack and app images to the MCU.
To be able to make aggressive modifications to the sample code, developers have to gain detailed knowledge about the layered structure of BLE stack. For elementary tasks such as temperature reading/notification, we can focus on only two files : PROFILES/simple_gatt_profile.c(.h) and Application/simple_peripheral.c(.h)
All Bluetooth applications offer a certain type of service, each consists of a set of characteristics. The simple peripheral profile defines one simple service, with the UUID of 0xFFF0, which consists of 5 characteristics. This service is specified in simple_gatt_profile.c. A summary of the simple service is listed as follows.
Name | Data Size | UUID | Description | Property |
---|---|---|---|---|
simplePeripheralChar1 | 1 | 0xFFF1 | Characteristics 1 | Read & Write |
simplePeripheralChar2 | 1 | 0xFFF2 | Characteristics 2 | Read only |
simplePeripheralChar3 | 1 | 0xFFF3 | Characteristics 3 | Write only |
simplePeripheralChar4 | 1 | 0xFFF4 | Characteristics 4 | Notify |
simplePeripheralChar5 | 5 | 0xFFF5 | Characteristics 5 | Read only |
The five characteristics have different properties and serve as examples for various user cases. For example, the MCU can use simplePeripheralChar4 to notify its clients, upstream hosts, about the change of information.
To define a Bluetooth service, one has to construct an Attribute Table.
/*********************************************************************
* Profile Attributes - Table
*/
static gattAttribute_t simpleProfileAttrTbl[SERVAPP_NUM_ATTR_SUPPORTED] =
{
// Simple Profile Service
{
{ ATT_BT_UUID_SIZE, primaryServiceUUID }, /* type */
GATT_PERMIT_READ, /* permissions */
0, /* handle */
(uint8 *)&simpleProfileService /* pValue */
},
// Characteristic 1 Declaration
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&simpleProfileChar1Props
},
// Characteristic Value 1
{
{ ATT_UUID_SIZE, simpleProfilechar1UUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
&simpleProfileChar1
},
// Characteristic 1 User Description
{
{ ATT_BT_UUID_SIZE, charUserDescUUID },
GATT_PERMIT_READ,
0,
simpleProfileChar1UserDesp
},
...
};
The attribute table begins with a default 'primaryServiceUUID', which specifies the UUID of the service (0xFFF0 in this case). It then is followed by declarations of all characteristics that consist the service. Each characteristics has several attributes, namely access permission, value and user description, etc. This table is later registered with the BLE stack.
// Register GATT attribute list and CBs with GATT Server App
status = GATTServApp_RegisterService( simpleProfileAttrTbl,
GATT_NUM_ATTRS( simpleProfileAttrTbl ),
GATT_MAX_ENCRYPT_KEY_SIZE,
&simpleProfileCBs );
On registration of the service, developers have to provide three callback function for 'Read', 'Write' and 'Authorization' of the characteristics. We can find in the sample code the list of callback functions.
/*********************************************************************
* PROFILE CALLBACKS
*/
// Simple Profile Service Callbacks
// Note: When an operation on a characteristic requires authorization and
// pfnAuthorizeAttrCB is not defined for that characteristic's service, the
// Stack will report a status of ATT_ERR_UNLIKELY to the client. When an
// operation on a characteristic requires authorization the Stack will call
// pfnAuthorizeAttrCB to check a client's authorization prior to calling
// pfnReadAttrCB or pfnWriteAttrCB, so no checks for authorization need to be
// made within these functions.
CONST gattServiceCBs_t simpleProfileCBs =
{
simpleProfile_ReadAttrCB, // Read callback function pointer
simpleProfile_WriteAttrCB, // Write callback function pointer
NULL // Authorization callback function pointer
};
So, simpleProfile_ReadAttrCB will be called once service client sends a read request over the Bluetooth connection. Similarly, simpleProfile_WriteAttrCB will be called when a write request is made. Understanding these two functions is key to success of project customization.
Below is the read callback function.
/*********************************************************************
* @fn simpleProfile_ReadAttrCB
*
* @brief Read an attribute.
*
* @param connHandle - connection message was received on
* @param pAttr - pointer to attribute
* @param pValue - pointer to data to be read
* @param pLen - length of data to be read
* @param offset - offset of the first octet to be read
* @param maxLen - maximum length of data to be read
* @param method - type of read message
*
* @return SUCCESS, blePending or Failure
*/
static bStatus_t simpleProfile_ReadAttrCB(uint16_t connHandle,
gattAttribute_t *pAttr,
uint8_t *pValue, uint16_t *pLen,
uint16_t offset, uint16_t maxLen,
uint8_t method)
{
bStatus_t status = SUCCESS;
// If attribute permissions require authorization to read, return error
if ( gattPermitAuthorRead( pAttr->permissions ) )
{
// Insufficient authorization
return ( ATT_ERR_INSUFFICIENT_AUTHOR );
}
// Make sure it's not a blob operation (no attributes in the profile are long)
if ( offset > 0 )
{
return ( ATT_ERR_ATTR_NOT_LONG );
}
uint16 uuid = 0;
if ( pAttr->type.len == ATT_UUID_SIZE )
// 128-bit UUID
uuid = BUILD_UINT16( pAttr->type.uuid[12], pAttr->type.uuid[13]);
else
uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);
switch ( uuid )
{
// No need for "GATT_SERVICE_UUID" or "GATT_CLIENT_CHAR_CFG_UUID" cases;
// gattserverapp handles those reads
// characteristics 1 and 2 have read permissions
// characteritisc 3 does not have read permissions; therefore it is not
// included here
// characteristic 4 does not have read permissions, but because it
// can be sent as a notification, it is included here
case SIMPLEPROFILE_CHAR2_UUID:
*pLen = SIMPLEPROFILE_CHAR2_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR2_LEN );
break;
case SIMPLEPROFILE_CHAR1_UUID:
*pLen = SIMPLEPROFILE_CHAR1_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR1_LEN );
break;
case SIMPLEPROFILE_CHAR4_UUID:
*pLen = SIMPLEPROFILE_CHAR4_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR4_LEN );
break;
case SIMPLEPROFILE_CHAR5_UUID:
*pLen = SIMPLEPROFILE_CHAR5_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR5_LEN );
break;
default:
// Should never get here! (characteristics 3 and 4 do not have read permissions)
*pLen = 0;
status = ATT_ERR_ATTR_NOT_FOUND;
break;
}
return ( status );
}
I have slightly modified the code from its original version. This function takes 7 parameters, which are explained in the header comments. The function starts by checking the access permission of the attribute, e.g. whether it has read permission. Then it checks if this is a segment read of a larger blob read request by testing the condition 'if (offset > 0)'. Obviously, the function does not support blob read for now. Next, the UUID of the requested attribute is extracted. There are two types of UUID: 16-bit and 128-bit. While the sample code defines all characteristics using 16-bit UUIDs, the 128-bit UUID is more universal and more commonly used in upstream hosts like PC and smartphones. Therefore, several lines of code are used to convert 128-bit of UUID to 16-bit UUID.
uint16 uuid = 0;
if ( pAttr->type.len == ATT_UUID_SIZE )
// 128-bit UUID
uuid = BUILD_UINT16( pAttr->type.uuid[12], pAttr->type.uuid[13]);
else
uuid = BUILD_UINT16( pAttr->type.uuid[0], pAttr->type.uuid[1]);
Finally, after we get the UUID, we can determine which attribute is requested. Then the remaining job at developers' side is to copy the value of requested attribute to the destination pointer 'pValue'.
switch ( uuid )
{
case SIMPLEPROFILE_CHAR1_UUID:
*pLen = SIMPLEPROFILE_CHAR1_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR1_LEN );
break;
case SIMPLEPROFILE_CHAR2_UUID:
*pLen = SIMPLEPROFILE_CHAR2_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR2_LEN );
break;
case SIMPLEPROFILE_CHAR4_UUID:
*pLen = SIMPLEPROFILE_CHAR4_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR4_LEN );
break;
case SIMPLEPROFILE_CHAR5_UUID:
*pLen = SIMPLEPROFILE_CHAR5_LEN;
VOID memcpy( pValue, pAttr->pValue, SIMPLEPROFILE_CHAR5_LEN );
break;
default:
*pLen = 0;
status = ATT_ERR_ATTR_NOT_FOUND;
break;
}
The write callback function is similar except that there is a special type of write with UUID of GATT_CLIENT_CHAR_CFG_UUID. This is upstream host's request to register for characteristics notification or indication. Simply call the API GATTServApp_ProcessCCCWriteReq to pass the request to the BLE stack.
case GATT_CLIENT_CHAR_CFG_UUID:
status = GATTServApp_ProcessCCCWriteReq( connHandle, pAttr, pValue, len,
offset, GATT_CLIENT_CFG_NOTIFY | GATT_CLIENT_CFG_INDICATE ); // allow client to request notification or indication features
break;
The application side of the code on the MCU may want to be notified with any change to write-permitted characteristics. Developers can implement this notification the way they like. In the sample code, callback function is used.
// If a charactersitic value changed then callback function to notify application of change
if ( (notifyApp != 0xFF ) && simpleProfile_AppCBs && simpleProfile_AppCBs->pfnSimpleProfileChange )
{
simpleProfile_AppCBs->pfnSimpleProfileChange( notifyApp );
}
On the other hand, if the BLE peripheral wants to notify upstream hosts of any change in its characteristic, it can call the API GATTServApp_ProcessCharCfg. This API is demonstrated in the function SimpleProfile_SetParameter.
/*********************************************************************
* @fn SimpleProfile_SetParameter
*
* @brief Set a Simple Profile parameter.
*
* @param param - Profile parameter ID
* @param len - length of data to write
* @param value - pointer to data to write. This is dependent on
* the parameter ID and WILL be cast to the appropriate
* data type (example: data type of uint16 will be cast to
* uint16 pointer).
*
* @return bStatus_t
*/
bStatus_t SimpleProfile_SetParameter( uint8 param, uint8 len, void *value )
{
bStatus_t ret = SUCCESS;
switch ( param )
{
case SIMPLEPROFILE_CHAR2:
if ( len == SIMPLEPROFILE_CHAR2_LEN )
{
VOID memcpy( simpleProfileChar2, value, SIMPLEPROFILE_CHAR2_LEN );
}
else
{
ret = bleInvalidRange;
}
break;
case SIMPLEPROFILE_CHAR3:
if ( len == sizeof ( uint8 ) )
{
simpleProfileChar3 = *((uint8*)value);
}
else
{
ret = bleInvalidRange;
}
break;
case SIMPLEPROFILE_CHAR1:
if ( len == SIMPLEPROFILE_CHAR1_LEN )
{
VOID memcpy( simpleProfileChar1, value, SIMPLEPROFILE_CHAR1_LEN );
}
else
{
ret = bleInvalidRange;
}
break;
case SIMPLEPROFILE_CHAR4:
if ( len == SIMPLEPROFILE_CHAR4_LEN )
{
//simpleProfileChar4 = *((uint8*)value);
VOID memcpy( simpleProfileChar4, value, SIMPLEPROFILE_CHAR4_LEN );
// See if Notification has been enabled
GATTServApp_ProcessCharCfg( simpleProfileChar4Config, simpleProfileChar4, FALSE,
simpleProfileAttrTbl, GATT_NUM_ATTRS( simpleProfileAttrTbl ),
INVALID_TASK_ID, simpleProfile_ReadAttrCB );
}
else
{
ret = bleInvalidRange;
}
break;
case SIMPLEPROFILE_CHAR5:
if ( len == SIMPLEPROFILE_CHAR5_LEN )
{
VOID memcpy( simpleProfileChar5, value, SIMPLEPROFILE_CHAR5_LEN );
}
else
{
ret = bleInvalidRange;
}
break;
default:
ret = INVALIDPARAMETER;
break;
}
return ( ret );
}
So if the simple peripheral application wants to notify the current value of SIMPLEPROFILE_CHAR4 to peer devices, it can simply call the SimpleProfile_SetParameter function.
In summary, PROFILES/simple_gatt_profile.c(.h) defines the content of the service that the BLE peripheral would like to present to its clients, as well as the ways that those characteristics in the service are accessed.
TI's BLE stack is running on top of a lite multi-threaded OS layer. To add a workload to the MCU, developers have to create a task first. simple_peripheral.c demonstrates the basic structure of a custom task, which includes the creation, initialization and housekeeping of the task. To begin with the very basic tasks like temperature reading and notification, we will focus on a few key functions below.
The beginning of the file defines a set of parameters that can affect the Bluetooth connection behaviors.
// Advertising interval when device is discoverable (units of 625us, 160=100ms)
#define DEFAULT_ADVERTISING_INTERVAL 160
// Limited discoverable mode advertises for 30.72s, and then stops
// General discoverable mode advertises indefinitely
#define DEFAULT_DISCOVERABLE_MODE GAP_ADTYPE_FLAGS_GENERAL
// Minimum connection interval (units of 1.25ms, 80=100ms) if automatic
// parameter update request is enabled
#define DEFAULT_DESIRED_MIN_CONN_INTERVAL 80
// Maximum connection interval (units of 1.25ms, 800=1000ms) if automatic
// parameter update request is enabled
#define DEFAULT_DESIRED_MAX_CONN_INTERVAL 400
// Slave latency to use if automatic parameter update request is enabled
#define DEFAULT_DESIRED_SLAVE_LATENCY 0
// Supervision timeout value (units of 10ms, 1000=10s) if automatic parameter
// update request is enabled
#define DEFAULT_DESIRED_CONN_TIMEOUT 1000
// Whether to enable automatic parameter update request when a connection is
// formed
#define DEFAULT_ENABLE_UPDATE_REQUEST TRUE
// Connection Pause Peripheral time value (in seconds)
#define DEFAULT_CONN_PAUSE_PERIPHERAL 6
// How often to perform periodic event (in msec)
#define SBP_PERIODIC_EVT_PERIOD 1000
Parameters DEFAULT_DESIRED_MIN_CONN_INTERVAL, DEFAULT_DESIRED_MAX_CONN_INTERVAL and DEFAULT_DESIRED_SLAVE_LATENCY together define the connections interval of a Bluetooth connection, which is how frequently a pair of devices exchange information. A lower connection interval means a more responsive behavior but also higher power consumption.
Parameter DEFAULT_DESIRED_CONN_TIMEOUT defines how long to receive a peer response before a connection is deemed lost. Parameter DEFAULT_ENABLE_UPDATE_REQUEST defines if the slave device is allowed to change connection interval during run-time. It is useful in terms of power saving to have different connection parameters for busy and idle phases.
The parameter SBP_PERIODIC_EVT_PERIOD defines the period of a clock event that will allow the task executes a function call periodically. This is the perfect place for us to add the code to read temperature and notify the service clients.
The periodic clock is initiated in the SimpleBLEPeripheral_init function.
// Create one-shot clocks for internal periodic events.
Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);
This will create a clock with a period of SBP_PERIODIC_EVT_PERIOD. And on timeout, will call the function of SimpleBLEPeripheral_clockHandler with parameter SBP_PERIODIC_EVT. The clock event can then be triggered by
Util_startClock(&periodicClock);
Searching for the keyword Util_startClock, we can find that this periodic clock is first triggered on the GAPROLE_CONNECTED event (inside the SimpleBLEPeripheral_processStateChangeEvt function), which means the task will start a periodic routine once it establishes a connection with a host.
When the periodic clock times out, its registered callback function will be called.
/*********************************************************************
* @fn SimpleBLEPeripheral_clockHandler
*
* @brief Handler function for clock timeouts.
*
* @param arg - event type
*
* @return None.
*/
static void SimpleBLEPeripheral_clockHandler(UArg arg)
{
// Store the event.
events |= arg;
// Wake up the application.
Semaphore_post(sem);
}
This function set a flag in the events vector and activate the application from the OS task list. Notice that we do not do any specific user workload in this callback function, because it is NOT recommended. User workload often involve calls to BLE stack APIs. Doing BLE stack API calls inside a callback functions often result in system exceptions. Instead, we set a flag in the events vector of the task and wait for it to be processed later in the application context. The entry point for the example task is simpleBLEPeripheral_taskFxn().
/*********************************************************************
* @fn SimpleBLEPeripheral_taskFxn
*
* @brief Application task entry point for the Simple BLE Peripheral.
*
* @param a0, a1 - not used.
*
* @return None.
*/
static void SimpleBLEPeripheral_taskFxn(UArg a0, UArg a1)
{
// Initialize application
SimpleBLEPeripheral_init();
// Application main loop
for (;;)
{
// Waits for a signal to the semaphore associated with the calling thread.
// Note that the semaphore associated with a thread is signaled when a
// message is queued to the message receive queue of the thread or when
// ICall_signal() function is called onto the semaphore.
ICall_Errno errno = ICall_wait(ICALL_TIMEOUT_FOREVER);
if (errno == ICALL_ERRNO_SUCCESS)
{
ICall_EntityID dest;
ICall_ServiceEnum src;
ICall_HciExtEvt *pMsg = NULL;
if (ICall_fetchServiceMsg(&src, &dest,
(void **)&pMsg) == ICALL_ERRNO_SUCCESS)
{
uint8 safeToDealloc = TRUE;
if ((src == ICALL_SERVICE_CLASS_BLE) && (dest == selfEntity))
{
ICall_Stack_Event *pEvt = (ICall_Stack_Event *)pMsg;
// Check for BLE stack events first
if (pEvt->signature == 0xffff)
{
if (pEvt->event_flag & SBP_CONN_EVT_END_EVT)
{
// Try to retransmit pending ATT Response (if any)
SimpleBLEPeripheral_sendAttRsp();
}
}
else
{
// Process inter-task message
safeToDealloc = SimpleBLEPeripheral_processStackMsg((ICall_Hdr *)pMsg);
}
}
if (pMsg && safeToDealloc)
{
ICall_freeMsg(pMsg);
}
}
// If RTOS queue is not empty, process app message.
while (!Queue_empty(appMsgQueue))
{
sbpEvt_t *pMsg = (sbpEvt_t *)Util_dequeueMsg(appMsgQueue);
if (pMsg)
{
// Process message.
SimpleBLEPeripheral_processAppMsg(pMsg);
// Free the space from the message.
ICall_free(pMsg);
}
}
}
if (events & SBP_PERIODIC_EVT)
{
events &= ~SBP_PERIODIC_EVT;
Util_startClock(&periodicClock);
// Perform periodic application task
SimpleBLEPeripheral_performPeriodicTask();
}
}
}
It is an infinite loop that keeps polling the task's stack and application message queues. It also checks its events vector for various flags. That's where the periodical routine is actually executed. On discovery of a SBP_PERIODIC_EVT, the task function first clears the flag, starts the same timer immediately and calls the routine function SimpleBLEPeripheral_performPeriodicTask();
/*********************************************************************
* @fn SimpleBLEPeripheral_performPeriodicTask
*
* @brief Perform a periodic application task. This function gets called
* every five seconds (SBP_PERIODIC_EVT_PERIOD). In this example,
* the value of the third characteristic in the SimpleGATTProfile
* service is retrieved from the profile, and then copied into the
* value of the fourth characteristic.
*
* @param None.
*
* @return None.
*/
static void SimpleBLEPeripheral_performPeriodicTask(void)
{
uint8_t newValue[SIMPLEPROFILE_CHAR4_LEN];
// user codes to do specific work like reading the temperature
// .....
SimpleProfile_SetParameter(SIMPLEPROFILE_CHAR4, SIMPLEPROFILE_CHAR4_LEN,
newValue);
}
Inside the periodic function, we run our very specific job of reading temperature, generating UART requests etc.. Then we call the SimpleProfile_SetParameter() API to communicate the information to service clients through Bluetooth connection. The BLE stack takes care of all low level jobs from maintaining the wireless connection to transmitting message over the Bluetooth link. All developers need to do is to gather the application specific data and update them to corresponding characteristics in a service table.
Finally, when a write request is performed on a write-permitted characteristics, a callback function will be evoked.
static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}
Again, this callback function only enqueues an application message for the user task, which will be handled later in the application context.
static void SimpleBLEPeripheral_processCharValueChangeEvt(uint8_t paramID)
{
uint8_t newValue[SIMPLEPROFILE_CHAR1_LEN];
switch(paramID)
{
case SIMPLEPROFILE_CHAR1:
SimpleProfile_GetParameter(SIMPLEPROFILE_CHAR1, &newValue[0]);
ProcessUserCmd(newValue[0], NULL);
break;
case SIMPLEPROFILE_CHAR3:
break;
default:
// should not reach here!
break;
}
}
In the above example, when the SIMPLEPROFILE_CHAR1 is written, the user code will first fetch the new value by calling SimpleProfile_GetParameter(), and then parse the data for user defined commands.
In summary, the simple_peripheral.c shows an example of how to create user task for custom workloads. A basic way to schedule application workload is by periodic clock event. Developers only need to process information to/from the characteristics in the service table while the BLE stack takes care of the rest of communicating the information from the service table to peer devices (or vice versa) through Bluetooth connection.