bluetooth
Empieza con la pila BLE de TI
Buscar..
Conexión a dispositivos esclavos BLE
Introducción
Los SoC de la serie CC26XX de Texas Instruments (TI) son MCU inalámbricas disponibles para aplicaciones Bluetooth de baja energía (BLE). Junto con los MCU, TI ofrece una pila de software completa que proporciona la API y los códigos de muestra necesarios para ayudar a los desarrolladores a comenzar rápidamente con la cadena de herramientas. Sin embargo, para los principiantes, siempre existe la pregunta de dónde comenzar frente a una larga lista de documentos y códigos de referencia. Esta nota tiene como objetivo anotar los pasos necesarios que se necesitan para poner en marcha el primer proyecto.
El Perfil Periférico Simple es el ejemplo 'Hola Mundo' de la pila BLE, donde la MCU actúa como un periférico BLE para los hosts ascendentes, o clientes del servicio BLE, como PC y teléfonos inteligentes. Las aplicaciones comunes del mundo real incluyen: auriculares Bluetooth, sensor de temperatura Bluetooth, etc.
Antes de comenzar, primero debemos reunir herramientas básicas de software y hardware con el fin de programar y depurar.
Pila BLE
Descargue e instale BLE-STACK-2-2-0 de TI desde el sitio web oficial. Supongamos que está instalado en la ubicación predeterminada 'C: \ ti'.
IDE - hay dos opciones:
IAR Embedded Workbench para ARM. Esta es una herramienta comercial con un período de evaluación gratuito de 30 días.
Code Composer Studio (CCS) de TI. IDE oficial de TI y ofrece licencia gratuita. En este ejemplo utilizaremos CCS V6.1.3.
Herramienta de programación de hardware
Recomiende el dispositivo JTAG de interfaz USB XDS100 de TI.
Proyecto de ejemplo de importación en CCS
El código de ejemplo de Perfil Periférico Simple viene con la instalación de BLE-Stack. Siga los pasos a continuación para importar este proyecto de ejemplo a CCS.
- Inicie CCS, cree una carpeta de área de trabajo. Luego Archivo-> Importar. En 'Seleccionar un origen de importación', seleccione la opción 'Code Compose Studio -> CCS Projects' y haga clic en 'Siguiente'.
- Vaya a 'C: \ ti \ simplelink \ ble_sdk_2_02_00_31 \ examples \ cc2650em \ simple_peripheral \ ccs'. Se descubrirán dos proyectos. Seleccione todo y marque ambas opciones a continuación. Luego haga clic en 'Finalizar'. Al copiar proyectos en el área de trabajo, deja la configuración del proyecto original sin cambios para todas las modificaciones siguientes.
El ejemplo de Perfil Periférico Simple incluye dos proyectos:
- simple_peripheral_cc2650em_app
- simple_peripheral_cc2650em_stack
'cc2650em' es el nombre en código de la placa de evaluación cc2650 de TI. El proyecto _stack incluye los códigos y el binario de BEL-Stack-2-2-0 de TI, que maneja la publicidad Bluetooth, la comunicación, la sincronización de frecuencia, etc. Esta es la parte del código que es relativamente estable y no quiere serlo. Tocado por los desarrolladores la mayor parte del tiempo. El proyecto _app es donde los desarrolladores implementan sus propias tareas y el servicio BLE.
Construir y descargar
Haga clic en los menús 'Proyecto-> Construir todo' para construir ambos proyectos. Si el compilador informa de algún tipo de error interno en la vinculación, intente deshabilitar la opción 'compress_dwarf' para el vinculador mediante:
- Haga clic derecho en el proyecto y seleccione 'Propiedades'.
- en 'Build-> ARM Linker', haga clic en el botón 'Editar indicadores'.
- modifique la última opción a '--compress_dwarf = off'.
Una vez que ambos proyectos se hayan creado con éxito, haga clic en 'Ejecutar-> depurar' por separado para descargar tanto la pila como las imágenes de la aplicación en la MCU.
Toca el codigo
Para poder realizar modificaciones agresivas en el código de muestra, los desarrolladores deben obtener un conocimiento detallado de la estructura en capas de la pila BLE. Para tareas elementales como la lectura / notificación de temperatura, podemos enfocarnos en solo dos archivos: PERFILES / simple_gatt_profile.c (.h) y Application / simple_peripheral.c (.h)
simple_gatt_profile.c
Todas las aplicaciones de Bluetooth ofrecen un cierto tipo de servicio, cada uno consiste en un conjunto de características. El perfil periférico simple define un servicio simple, con el UUID de 0xFFF0, que consta de 5 características. Este servicio se especifica en simple_gatt_profile.c. Un resumen del servicio simple se enumera a continuación.
Nombre | Tamaño de datos | UUID | Descripción | Propiedad |
---|---|---|---|---|
simplePeripheralChar1 | 1 | 0xFFF1 | Caracteristicas 1 | Leer escribir |
simplePeripheralChar2 | 1 | 0xFFF2 | Caracteristicas 2 | Solo lectura |
simplePeripheralChar3 | 1 | 0xFFF3 | Caracteristicas 3 | Escribir solamente |
simplePeripheralChar4 | 1 | 0xFFF4 | Caracteristicas 4 | Notificar |
simplePeripheralChar5 | 5 | 0xFFF5 | Caracteristicas 5 | Solo lectura |
Las cinco características tienen propiedades diferentes y sirven como ejemplos para varios casos de usuarios. Por ejemplo, la MCU puede usar simplePeripheralChar4 para notificar a sus clientes, hosts ascendentes, sobre el cambio de información.
Para definir un servicio Bluetooth, uno tiene que construir una tabla de atributos.
/*********************************************************************
* 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
},
...
};
La tabla de atributos comienza con un 'PrimaryServiceUUID' predeterminado, que especifica el UUID del servicio (0xFFF0 en este caso). Luego es seguido por declaraciones de todas las características que constituyen el servicio. Cada característica tiene varios atributos, a saber, permiso de acceso, valor y descripción del usuario, etc. Esta tabla se registra posteriormente con la pila BLE.
// Register GATT attribute list and CBs with GATT Server App
status = GATTServApp_RegisterService( simpleProfileAttrTbl,
GATT_NUM_ATTRS( simpleProfileAttrTbl ),
GATT_MAX_ENCRYPT_KEY_SIZE,
&simpleProfileCBs );
En el registro del servicio, los desarrolladores tienen que proporcionar tres funciones de devolución de llamada para 'Leer', 'Escribir' y 'Autorización' de las características. Podemos encontrar en el código de ejemplo la lista de funciones de devolución de llamada.
/*********************************************************************
* 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
};
Por lo tanto, se llamará a simpleProfile_ReadAttrCB una vez que el cliente de servicio envíe una solicitud de lectura a través de la conexión Bluetooth. De manera similar, se llamará a simpleProfile_WriteAttrCB cuando se realice una solicitud de escritura. Comprender estas dos funciones es clave para el éxito de la personalización del proyecto.
A continuación se muestra la función de devolución de llamada de lectura.
/*********************************************************************
* @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 );
}
He modificado ligeramente el código de su versión original. Esta función toma 7 parámetros, que se explican en los comentarios del encabezado. La función comienza verificando el permiso de acceso del atributo, por ejemplo, si tiene permiso de lectura. Luego verifica si se trata de una lectura de segmento de una solicitud de lectura de blob más grande al probar la condición 'if (offset> 0)'. Obviamente, la función no admite la lectura de blob por ahora. A continuación, se extrae el UUID del atributo solicitado. Hay dos tipos de UUID: 16 bits y 128 bits. Si bien el código de muestra define todas las características utilizando UUID de 16 bits, el UUID de 128 bits es más universal y se usa más comúnmente en hosts ascendentes como PC y teléfonos inteligentes. Por lo tanto, se utilizan varias líneas de código para convertir 128 bits de UUID en UUID de 16 bits.
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]);
Finalmente, después de obtener el UUID, podemos determinar qué atributo se solicita. Luego, el trabajo restante en el lado de los desarrolladores es copiar el valor del atributo solicitado al puntero de destino '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;
}
La función de devolución de llamada de escritura es similar, excepto que hay un tipo especial de escritura con UUID de GATT_CLIENT_CHAR_CFG_UUID. Esta es la solicitud del host ascendente para registrarse para la notificación o indicación de características. Simplemente llame a la API GATTServApp_ProcessCCCWriteReq para pasar la solicitud a la pila BLE.
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;
El lado de la aplicación del código en la MCU puede querer ser notificado con cualquier cambio en las características permitidas de escritura. Los desarrolladores pueden implementar esta notificación a su gusto. En el código de ejemplo, se utiliza la función de devolución de llamada.
// 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 );
}
Por otro lado, si el periférico BLE desea notificar a los hosts ascendentes de cualquier cambio en su característica, puede llamar a la API GATTServApp_ProcessCharCfg. Esta API se demuestra en la función 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 );
}
Por lo tanto, si la aplicación periférica simple desea notificar el valor actual de SIMPLEPROFILE_CHAR4 a los dispositivos pares, puede simplemente llamar a la función SimpleProfile_SetParameter.
En resumen, PERFILES / simple_gatt_profile.c (.h) define el contenido del servicio que el periférico BLE le gustaría presentar a sus clientes, así como las formas en que se accede a esas características en el servicio.
simple_peripheral.c
La pila BLE de TI se está ejecutando en la parte superior de una capa de SO multihilo lite. Para agregar una carga de trabajo a la MCU, los desarrolladores deben crear una tarea primero. simple_peripheral.c demuestra la estructura básica de una tarea personalizada, que incluye la creación, inicialización y mantenimiento de la tarea. Para comenzar con las tareas muy básicas, como la lectura de la temperatura y la notificación, nos centraremos en algunas de las siguientes funciones clave a continuación.
El comienzo del archivo define un conjunto de parámetros que pueden afectar los comportamientos de conexión de Bluetooth.
// 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
Los parámetros DEFAULT_DESIRED_MIN_CONN_INTERVAL, DEFAULT_DESIRED_MAX_CONN_INTERVAL y DEFAULT_DESIRED_SLAVE_LATENCY definen juntos el intervalo de conexiones de una conexión Bluetooth, que es la frecuencia con la que un par de dispositivos intercambian información. Un menor intervalo de conexión significa un comportamiento más sensible pero también un mayor consumo de energía.
El parámetro DEFAULT_DESIRED_CONN_TIMEOUT define cuánto tiempo se debe recibir una respuesta de un par antes de que una conexión se considere perdida. El parámetro DEFAULT_ENABLE_UPDATE_REQUEST define si el dispositivo esclavo puede cambiar el intervalo de conexión durante el tiempo de ejecución. Es útil en términos de ahorro de energía tener diferentes parámetros de conexión para fases ocupadas e inactivas.
El parámetro SBP_PERIODIC_EVT_PERIOD define el período de un evento de reloj que permitirá que la tarea ejecute una llamada de función periódicamente. Este es el lugar perfecto para que agreguemos el código para leer la temperatura y notificar a los clientes del servicio.
El reloj periódico se inicia en la función SimpleBLEPeripheral_init.
// Create one-shot clocks for internal periodic events.
Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);
Esto creará un reloj con un período de SBP_PERIODIC_EVT_PERIOD. Y en el tiempo de espera, llamará a la función de SimpleBLEPeripheral_clockHandler con el parámetro SBP_PERIODIC_EVT. El evento del reloj puede ser activado por
Util_startClock(&periodicClock);
Al buscar la palabra clave Util_startClock, podemos encontrar que este reloj periódico se activa primero en el evento GAPROLE_CONNECTED (dentro de la función SimpleBLEPeripheral_processStateChangeEvt), lo que significa que la tarea iniciará una rutina periódica una vez que establezca una conexión con un host.
Cuando se agote el tiempo del reloj periódico, se llamará a su función de devolución de llamada registrada.
/*********************************************************************
* @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);
}
Esta función establece un indicador en el vector de eventos y activa la aplicación desde la lista de tareas del sistema operativo. Tenga en cuenta que no hacemos ninguna carga de trabajo específica del usuario en esta función de devolución de llamada, porque NO se recomienda. La carga de trabajo del usuario a menudo involucra llamadas a las API de la pila BLE. Hacer llamadas a la API de la pila BLE dentro de las funciones de devolución de llamada a menudo resulta en excepciones del sistema. En su lugar, establecemos una marca en el vector de eventos de la tarea y esperamos a que se procese más adelante en el contexto de la aplicación. El punto de entrada para la tarea de ejemplo es 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();
}
}
}
Es un bucle infinito que sigue sondeando la pila de la tarea y las colas de mensajes de la aplicación. También comprueba su vector de eventos para varias banderas. Ahí es donde realmente se ejecuta la rutina periódica. Al descubrir un SBP_PERIODIC_EVT, la función de tarea primero borra el indicador, inicia el mismo temporizador inmediatamente y llama a la función de rutina 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);
}
Dentro de la función periódica, ejecutamos nuestro trabajo muy específico de lectura de temperatura, generación de solicitudes UART, etc. Luego, llamamos a la API SimpleProfile_SetParameter () para comunicar la información a los clientes de servicio a través de la conexión Bluetooth. La pila BLE se encarga de todos los trabajos de bajo nivel desde mantener la conexión inalámbrica hasta transmitir un mensaje a través del enlace Bluetooth. Todo lo que deben hacer los desarrolladores es reunir los datos específicos de la aplicación y actualizarlos a las características correspondientes en una tabla de servicios.
Finalmente, cuando se realiza una solicitud de escritura en unas características permitidas de escritura, se provocará una función de devolución de llamada.
static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}
Nuevamente, esta función de devolución de llamada solo pone en cola un mensaje de aplicación para la tarea del usuario, que se manejará más adelante en el contexto de la aplicación.
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;
}
}
En el ejemplo anterior, cuando se escribe SIMPLEPROFILE_CHAR1, el código de usuario primero obtendrá el nuevo valor llamando a SimpleProfile_GetParameter (), y luego analizará los datos de los comandos definidos por el usuario.
En resumen, simple_peripheral.c muestra un ejemplo de cómo crear tareas de usuario para cargas de trabajo personalizadas. Una forma básica de programar la carga de trabajo de la aplicación es mediante un evento de reloj periódico. Los desarrolladores solo necesitan procesar información de las características en la tabla de servicios, mientras que la pila BLE se encarga del resto de la comunicación de la información de la tabla de servicios a los dispositivos pares (o viceversa) a través de la conexión Bluetooth.
Conexión de sensores del mundo real
Para que los dispositivos esclavos BLE hagan un trabajo útil, los GPIO de la MCU inalámbrica están casi siempre involucrados. Por ejemplo, para leer la temperatura de un sensor externo, la funcionalidad ADC de los pines GPIO puede ser necesaria. La MCU CC2640 de TI cuenta con un máximo de 31 GPIO, según los diferentes tipos de empaquetado.
En el lado del hardware, CC2640 proporciona un amplio conjunto de funcionalidades periféricas como ADC, UARTS, SPI, SSI, I2C, etc. En el lado del software, la pila BLE de TI intenta ofrecer una interfaz de controlador uniforme e independiente del dispositivo para diferentes periféricos. Una interfaz de controlador uniforme puede mejorar la posibilidad de reutilización del código, pero por otro lado, también aumenta la pendiente de la curva de aprendizaje. En esta nota, usamos el controlador SPI como ejemplo y mostramos cómo integrar el controlador de software en las aplicaciones de usuario.
Flujo básico del controlador SPI
En la pila BLE de TI, un controlador periférico a menudo consta de tres partes: una especificación independiente del dispositivo de las API del controlador; una implementación específica del dispositivo de las API del controlador y una asignación de recursos de hardware.
Para el controlador SPI, su implementación de controlador incluye tres archivos:
- <ti / drivers / SPI.h> - esta es la especificación API independiente del dispositivo
- <ti / drivers / spi / SPICC26XXDMA.h> - esta es la implementación de la API específica de CC2640
- <ti / drivers / dma / UDMACC26XX.h> - este es el controlador uDMA requerido por el controlador SPI
(Nota: el mejor documento para los controladores periféricos de la pila BLE de TI se puede encontrar principalmente en sus archivos de encabezado, como SPICC26XXDMA.h en este caso)
Para comenzar a usar el controlador SPI, primero creemos un archivo c personalizado, a saber, sbp_spi.c, que incluya los tres archivos de encabezado anteriores. El siguiente paso natural es crear una instancia del controlador e iniciarlo. La instancia del controlador está encapsulada en la estructura de datos - SPI_Handle. Otra estructura de datos: SPI_Params se utiliza para especificar los parámetros clave para el controlador SPI, como la velocidad de bits, el modo de transferencia, 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);
}
El código de ejemplo anterior ejemplifica cómo inicializar la instancia de SPI_Handle. La API SPI_init () se debe llamar primero para inicializar las estructuras de datos internas. La función llamada SPI_Params_init (& spiParams) establece todos los campos de la estructura SPI_Params en valores predeterminados. Entonces los desarrolladores pueden modificar los parámetros clave para adaptarse a sus casos específicos. Por ejemplo, el código anterior establece que el controlador SPI funcione en modo maestro con una velocidad de bits de 800 kbps y utiliza un método no bloqueante para procesar cada transacción, de modo que cuando se completa una transacción, se llamará a la función de devolución de llamada sbp_spiCallback.
Finalmente, una llamada al SPI_open () abre el controlador SPI de hardware y devuelve un identificador para las transacciones SPI posteriores. El SPI_open () toma dos argumentos, el primero es el ID del controlador SPI. CC2640 cuenta con dos controladores de hardware SPI en el chip, por lo tanto, los argumentos de esta ID serán 0 o 1 como se define a continuación. El segundo argumento son los parámetros deseados para el controlador SPI.
/*!
* @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;
Luego de la apertura exitosa de SPI_Handle, los desarrolladores pueden iniciar transacciones SPI inmediatamente. Cada transacción SPI se describe utilizando la estructura de datos - 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;
Por ejemplo, para iniciar una transacción de escritura en el bus SPI, los desarrolladores deben preparar un 'txBuf' lleno de datos para transmitir y establecer la variable 'count' a la longitud de los bytes de datos que se enviarán. Finalmente, una llamada a SPI_transfer (spiHandle, spiTrans) le indica al controlador SPI que inicie la transacción.
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);
}
Debido a que SPI es un protocolo dúplex que tanto la transmisión como la recepción se producen al mismo tiempo, cuando finaliza una transacción de escritura, sus datos de respuesta correspondientes ya están disponibles en el 'rxBuf'.
Dado que configuramos el modo de transferencia en modo de devolución de llamada, siempre que se complete una transacción, se llamará a la función de devolución de llamada registrada. Aquí es donde manejamos los datos de respuesta o iniciamos la siguiente transacción. (Nota: recuerde siempre no hacer más de lo necesario las llamadas de API dentro de una función de devolución de llamada).
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);
}
Configuración de pin de E / S
Hasta ahora, parece razonablemente sencillo utilizar el controlador SPI. Pero espere, ¿cómo se pueden conectar las llamadas de la API del software a las señales SPI físicas? Esto se realiza a través de tres estructuras de datos: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 y SPI_Config. Normalmente se crean instancias en una ubicación diferente, como '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}
};
La matriz SPI_Config tiene una entrada independiente para cada controlador SPI de hardware. Cada entrada tiene tres campos: fxnTablePtr, object y hwAttrs. El 'fxnTablePtr' es una tabla de puntos que apunta a las implementaciones específicas del dispositivo de la API del controlador.
El 'objeto' mantiene un registro de información como el estado del controlador, el modo de transferencia y la función de devolución de llamada del controlador. Este 'objeto' se mantiene automáticamente por el controlador.
Los 'hwAttrs' almacenan los datos reales de mapeo de recursos de hardware, por ejemplo, los pines IO para las señales SPI, el número de interrupción del hardware, la dirección base del controlador SPI, etc. La mayoría de los campos de los 'hwAttrs' están predefinidos y no pueden modificarse. Mientras que los pines IO de la interfaz se pueden asignar libremente a los casos de usuarios basados en. Nota: las MCU CC26XX desacoplan los pines IO de la funcionalidad periférica específica que cualquiera de los pines IO puede asignar a cualquier función periférica.
Por supuesto, los pines IO reales deben definirse primero en el '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
Como resultado, después de la configuración de la asignación de recursos de hardware, los desarrolladores pueden finalmente comunicarse con chips de sensores externos a través de la interfaz SPI.