Ricerca…


Connessione a dispositivi slave BLE

introduzione

I SoC CC26XX della serie Texas Instruments (TI) sono MCU wireless prontamente disponibili destinati alle applicazioni Bluetooth Low Energy (BLE). Insieme agli MCU, TI offre uno stack software completo che fornisce i codici API e di esempio necessari per aiutare rapidamente gli sviluppatori a iniziare con la catena di strumenti. Tuttavia, per i principianti, c'è sempre la domanda su dove iniziare di fronte a una lunga lista di documenti e codici di riferimento. Questa nota mira a registrare i passi necessari necessari per dare il via al primo progetto in corso.

Il Simple Peripheral Profile è l'esempio "Hello World" dello stack BLE, in cui MCU funge da periferica BLE per gli host upstream o client di servizio BLE, come PC e smartphone. Le comuni applicazioni del mondo reale includono: cuffia Bluetooth, sensore di temperatura Bluetooth, ecc.

Prima di iniziare, dobbiamo prima raccogliere strumenti software e hardware di base per la programmazione e il debug.

  1. Pila BLE

    Scarica e installa TI BLE-STACK-2-2-0 dal sito ufficiale. Supponiamo che sia installato nella posizione predefinita "C: \ ti".

  2. IDE - ci sono due opzioni:

  • Banco da lavoro embedded IAR per ARM. Questo è uno strumento commerciale con un periodo di valutazione gratuito di 30 giorni.

  • Code Composer Studio di TI (CCS). IDE ufficiale di TI e offre licenza gratuita. In questo esempio useremo CCS V6.1.3

  1. Strumento di programmazione hardware

    Consiglia il dispositivo JTAG con interfaccia USB XDS100 di TI.

Importa progetto di esempio in CCS

Il codice di esempio Simple Peripheral Profile viene fornito con l'installazione BLE-Stack. Seguire i passaggi seguenti per importare questo esempio di progetto in CCS.

  1. Avvia CCS, crea una cartella di lavoro. Quindi File-> Importa. Sotto 'Seleziona una fonte di importazione', seleziona l'opzione 'Code Compose Studio -> Progetti CCS' e fai clic su 'Avanti'. Importa progetto CCS
  2. Passare a "C: \ ti \ simplelink \ ble_sdk_2_02_00_31 \ examples \ cc2650em \ simple_peripheral \ ccs". Due progetti saranno scoperti. Seleziona tutto e seleziona entrambe le opzioni di seguito. Quindi fare clic su "Fine". Copiando i progetti nello spazio di lavoro, si lascia invariata l'impostazione originale del progetto per tutte le successive modifiche. inserisci la descrizione dell'immagine qui

L'esempio di Simple Peripheral Profile include due progetti:

  • simple_peripheral_cc2650em_app
  • simple_peripheral_cc2650em_stack

'cc2650em' è il nome in codice per la scheda di valutazione cc2650 di TI. Il progetto _stack include i codici e il binario di BEL-Stack-2-2-0 di TI, che gestisce la pubblicità Bluetooth, l'handshake, la sincronizzazione di frequenza, ecc. Questa è la parte del codice che è relativamente stabile e non vuole essere toccato dagli sviluppatori il più delle volte. Il progetto _app è il luogo in cui gli sviluppatori implementano i propri compiti e il servizio BLE.

Costruisci e scarica

Fai clic sui menu "Progetto-> Crea tutto" per creare entrambi i progetti. Se il compilatore segnala una sorta di errore interno sul collegamento, prova a disabilitare l'opzione 'compress_dwarf' per il linker:

  • fare clic con il tasto destro del mouse sul progetto e selezionare "Propoerties".
  • in 'Build-> ARM Linker', fai clic sul pulsante 'Modifica flag'.
  • modifica l'ultima opzione a '--compress_dwarf = off'.

Dopo che entrambi i progetti sono stati creati correttamente, fai clic su "Esegui-> esegui il debug" separatamente per scaricare sia le immagini dello stack che quelle delle app sull'MCU.

Tocca il codice

Per essere in grado di apportare modifiche aggressive al codice di esempio, gli sviluppatori devono acquisire conoscenze dettagliate sulla struttura a livelli dello stack BLE. Per compiti elementari come la lettura / notifica della temperatura, possiamo concentrarci solo su due file: PROFILI / simple_gatt_profile.c (.h) e Application / simple_peripheral.c (.h)

simple_gatt_profile.c

Tutte le applicazioni Bluetooth offrono un determinato tipo di servizio, ciascuna costituita da un insieme di caratteristiche. Il semplice profilo periferico definisce un servizio semplice, con l'UUID di 0xFFF0, che consiste di 5 caratteristiche. Questo servizio è specificato in simple_gatt_profile.c. Un riepilogo del servizio semplice è elencato come segue.

Nome Dimensione dei dati UUID Descrizione Proprietà
simplePeripheralChar1 1 0xFFF1 Caratteristiche 1 Leggere scrivere
simplePeripheralChar2 1 0xFFF2 Caratteristiche 2 Sola lettura
simplePeripheralChar3 1 0xFFF3 Caratteristiche 3 Scrivi solo
simplePeripheralChar4 1 0xFFF4 Caratteristiche 4 Notificare
simplePeripheralChar5 5 0xFFF5 Caratteristiche 5 Sola lettura

Le cinque caratteristiche hanno proprietà diverse e servono come esempi per vari casi di utenti. Ad esempio, MCU può utilizzare simplePeripheralChar4 per notificare ai suoi clienti, host a monte, la modifica delle informazioni.

Per definire un servizio Bluetooth, si deve costruire una tabella degli attributi.

/*********************************************************************
 * 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 tabella degli attributi inizia con un 'primaryServiceUUID' predefinito, che specifica l'UUID del servizio (0xFFF0 in questo caso). Poi è seguito da dichiarazioni di tutte le caratteristiche che consistono nel servizio. Ogni caratteristica ha diversi attributi, vale a dire il permesso di accesso, il valore e la descrizione dell'utente, ecc. Questa tabella viene successivamente registrata con lo stack 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 );

Al momento della registrazione del servizio, gli sviluppatori devono fornire tre funzioni di callback per "Leggi", "Scrivi" e "Autorizzazione" delle caratteristiche. Possiamo trovare nel codice di esempio l'elenco delle funzioni di callback.

/*********************************************************************
 * 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
};

Quindi, simpleProfile_ReadAttrCB verrà chiamato una volta che il client di servizio invia una richiesta di lettura tramite la connessione Bluetooth. Allo stesso modo, simpleProfile_WriteAttrCB verrà chiamato quando viene effettuata una richiesta di scrittura. Comprendere queste due funzioni è la chiave per il successo della personalizzazione del progetto.

Di seguito è riportata la funzione di callback di lettura.

/*********************************************************************
 * @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 );
}

Ho leggermente modificato il codice dalla sua versione originale. Questa funzione richiede 7 parametri, che sono spiegati nei commenti dell'intestazione. La funzione inizia controllando il permesso di accesso dell'attributo, ad esempio se ha il permesso di lettura. Quindi controlla se questa è una lettura di segmento di una richiesta di lettura del blob più grande testando la condizione 'if (offset> 0)'. Ovviamente, la funzione non supporta blob read per ora. Successivamente, viene estratto l'UUID dell'attributo richiesto. Esistono due tipi di UUID: 16 bit e 128 bit. Mentre il codice di esempio definisce tutte le caratteristiche utilizzando UUID a 16 bit, l'UUID a 128 bit è più universale e più comunemente utilizzato negli host upstream come PC e smartphone. Pertanto, vengono utilizzate diverse righe di codice per convertire l'UUID a 128 bit in UUID a 16 bit.

 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]);

Infine, dopo aver ottenuto l'UUID, possiamo determinare quale attributo è richiesto. Quindi il lavoro rimanente sul lato degli sviluppatori consiste nel copiare il valore dell'attributo richiesto nel puntatore di destinazione "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 funzione di callback di scrittura è simile tranne che esiste un tipo speciale di scrittura con UUID di GATT_CLIENT_CHAR_CFG_UUID. Questa è la richiesta dell'host upstream di registrarsi per la notifica o l'indicazione delle caratteristiche. Basta chiamare l'API GATTServApp_ProcessCCCWriteReq per passare la richiesta allo stack 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;

Il lato applicativo del codice sull'MCU potrebbe voler essere avvisato con qualsiasi modifica alle caratteristiche consentite dalla scrittura. Gli sviluppatori possono implementare questa notifica nel modo che preferiscono. Nel codice di esempio, viene utilizzata la funzione di callback.

  // 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 );
  }

D'altra parte, se la periferica BLE vuole notificare agli host upstream qualsiasi cambiamento nelle sue caratteristiche, può chiamare l'API GATTServApp_ProcessCharCfg. Questa API è dimostrata nella funzione 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 );
}

Quindi, se la semplice applicazione periferica vuole notificare il valore corrente di SIMPLEPROFILE_CHAR4 ai dispositivi peer, può semplicemente chiamare la funzione SimpleProfile_SetParameter.

In sintesi, PROFILES / simple_gatt_profile.c (.h) definisce il contenuto del servizio che la periferica BLE vorrebbe presentare ai propri client, nonché i modi in cui si accede a quelle caratteristiche del servizio.

simple_peripheral.c

Lo stack BLE di TI è in esecuzione su un layer OS multi-thread lite. Per aggiungere un carico di lavoro alla MCU, gli sviluppatori devono prima creare un'attività. simple_peripheral.c mostra la struttura di base di un'attività personalizzata, che include la creazione, l'inizializzazione e la gestione delle attività. Per iniziare con le attività di base come la lettura della temperatura e la notifica, ci concentreremo su alcune funzioni chiave di seguito.

L'inizio del file definisce una serie di parametri che possono influenzare i comportamenti di connessione 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

I parametri DEFAULT_DESIRED_MIN_CONN_INTERVAL, DEFAULT_DESIRED_MAX_CONN_INTERVAL e DEFAULT_DESIRED_SLAVE_LATENCY definiscono insieme l'intervallo di connessioni di una connessione Bluetooth, che è la frequenza con cui una coppia di dispositivi scambia informazioni. Un intervallo di connessione più basso significa un comportamento più reattivo ma anche un maggiore consumo di energia.

Il parametro DEFAULT_DESIRED_CONN_TIMEOUT definisce la durata della ricezione di una risposta peer prima che una connessione venga considerata persa. Il parametro DEFAULT_ENABLE_UPDATE_REQUEST definisce se il dispositivo slave può modificare l'intervallo di connessione durante l'esecuzione. È utile in termini di risparmio energetico per avere parametri di connessione diversi per le fasi di occupato e inattivo.

Il parametro SBP_PERIODIC_EVT_PERIOD definisce il periodo di un evento di clock che consentirà al task di eseguire periodicamente una chiamata di funzione. Questo è il posto perfetto per noi per aggiungere il codice per leggere la temperatura e informare i clienti del servizio.

L'orologio periodico viene avviato nella funzione SimpleBLEPeripheral_init.

  // Create one-shot clocks for internal periodic events.
  Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
                      SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);

Ciò creerà un orologio con un periodo di SBP_PERIODIC_EVT_PERIOD. E al timeout, chiamerà la funzione di SimpleBLEPeripheral_clockHandler con il parametro SBP_PERIODIC_EVT. L'evento di clock può quindi essere attivato da

Util_startClock(&periodicClock);

Cercando la parola chiave Util_startClock, possiamo scoprire che questo orologio periodico viene innescato per la prima volta sull'evento GAPROLE_CONNECTED (all'interno della funzione SimpleBLEPeripheral_processStateChangeEvt), il che significa che l'attività avvierà una routine periodica una volta stabilita una connessione con un host.

Quando l'orologio periodico scade, viene richiamata la sua funzione di callback registrata.

/*********************************************************************
 * @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);
}

Questa funzione imposta un flag nel vettore degli eventi e attiva l'applicazione dall'elenco delle attività del SO. Si noti che non eseguiamo alcun carico di lavoro specifico dell'utente in questa funzione di callback, perché NON è raccomandato. Il carico di lavoro degli utenti spesso comporta chiamate alle API dello stack BLE. Effettuare chiamate API stack BLE all'interno di una funzione di callback spesso genera eccezioni di sistema. Invece, impostiamo un flag nel vettore degli eventi dell'attività e aspettiamo che venga elaborato successivamente nel contesto dell'applicazione. Il punto di ingresso per l'attività di esempio è 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();
    }

  }
}

È un ciclo infinito che continua a interrogare lo stack delle attività e le code dei messaggi delle applicazioni. Controlla anche il vettore degli eventi per varie bandiere. È qui che la routine periodica viene effettivamente eseguita. Alla scoperta di un SBP_PERIODIC_EVT, la funzione compito cancella prima il flag, avvia immediatamente lo stesso timer e chiama la funzione di routine 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);
}

All'interno della funzione periodica, eseguiamo il nostro lavoro specifico di lettura della temperatura, generando richieste UART ecc. Quindi chiamiamo l'API SimpleProfile_SetParameter () per comunicare le informazioni ai client di servizio tramite la connessione Bluetooth. Lo stack BLE si occupa di tutti i lavori di basso livello dal mantenimento della connessione wireless alla trasmissione di messaggi tramite il collegamento Bluetooth. Tutti gli sviluppatori devono fare è raccogliere i dati specifici dell'applicazione e aggiornarli alle caratteristiche corrispondenti in una tabella di servizio.

Infine, quando una richiesta di scrittura viene eseguita su caratteristiche permesse per la scrittura, verrà richiamata una funzione di callback.

static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
  SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}

Ancora una volta, questa funzione di callback accoda solo un messaggio dell'applicazione per l'attività dell'utente, che verrà gestito successivamente nel contesto dell'applicazione.

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;
  }
}

Nell'esempio precedente, quando viene scritto SIMPLEPROFILE_CHAR1, il codice utente preleverà prima il nuovo valore chiamando SimpleProfile_GetParameter (), quindi analizzerà i dati per i comandi definiti dall'utente.

In breve, simple_peripheral.c mostra un esempio di come creare attività utente per carichi di lavoro personalizzati. Un modo base per pianificare il carico di lavoro dell'applicazione è l'evento di orologio periodico. Gli sviluppatori devono solo elaborare le informazioni da / per le caratteristiche nella tabella dei servizi mentre lo stack BLE si occupa del resto della comunicazione delle informazioni dalla tabella dei servizi ai dispositivi peer (o viceversa) tramite la connessione Bluetooth.

Collegamento dei sensori del mondo reale

Perché i dispositivi slave BLE possano svolgere qualsiasi lavoro utile, i GPIO della MCU wireless sono quasi sempre coinvolti. Ad esempio, per leggere la temperatura da un sensore esterno, potrebbe essere necessaria la funzionalità ADC dei pin GPIO. L'MCU CC2640 di TI presenta un massimo di 31 GPIO, a seconda del tipo di imballaggio.

Dal punto di vista hardware, CC2640 offre un ricco set di funzionalità periferiche come ADC, UARTS, SPI, SSI, I2C, ecc. Nel lato software, lo stack BLE di TI cerca di offrire un'interfaccia driver indipendente dalla periferica uniforme per diverse periferiche. Un'interfaccia driver uniforme può migliorare la possibilità di riusabilità del codice, ma d'altra parte aumenta anche la pendenza della curva di apprendimento. In questa nota, utilizziamo il controller SPI come esempio e mostriamo come integrare il driver del software in applicazioni utente.

Flusso base del driver SPI

Nello stack BLE di TI, un driver periferico è spesso costituito da tre parti: una specifica indipendente dal dispositivo delle API del driver; un'implementazione specifica del dispositivo delle API del driver e una mappatura della risorsa hardware.

Per il controller SPI, l'implementazione del driver prevede tre file:

  • <ti / drivers / SPI.h>: questa è la specifica dell'API indipendente dalla periferica
  • <ti / drivers / spi / SPICC26XXDMA.h> - questa è l'implementazione dell'API specifica per CC2640
  • <ti / drivers / dma / UDMACC26XX.h> - questo è il driver uDMA richiesto dal driver SPI

(Nota: il documento migliore per i driver periferici dello stack BLE di TI può essere trovato principalmente nei loro file di intestazione, come SPICC26XXDMA.h in questo caso)

Per iniziare a utilizzare il controller SPI, creiamo innanzitutto un file c personalizzato, ovvero sbp_spi.c, che include i tre file di intestazione sopra. Il naturale passo successivo è creare un'istanza del driver e avviarlo. L'istanza del driver è incapsulata nella struttura dati - SPI_Handle. Un'altra struttura dati: SPI_Params viene utilizzata per specificare i parametri chiave per il controller SPI, come bit rate, modalità di trasferimento, ecc.

#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);
}

Il codice di esempio sopra esemplificativo illustra come inizializzare l'istanza SPI_Handle. L'API SPI_init () deve essere chiamata prima per inizializzare le strutture di dati interne. La funzione chiama SPI_Params_init (& spiParams) imposta tutti i campi della struttura SPI_Params sui valori predefiniti. Quindi gli sviluppatori possono modificare i parametri chiave per soddisfare i loro casi specifici. Ad esempio, il codice precedente imposta il controller SPI per operare in modalità master con una velocità in bit di 800kbps e utilizza un metodo non bloccante per elaborare ogni transazione, in modo che quando una transazione viene completata, la funzione di callback sbp_spiCallback venga chiamata.

Infine, una chiamata a SPI_open () apre il controller SPI hardware e restituisce un handle per le transazioni SPI successive. SPI_open () accetta due argomenti, il primo è l'ID del controller SPI. CC2640 dispone di due controller SPI hardware on-chip, quindi gli argomenti ID saranno 0 o 1 come definito di seguito. Il secondo argomento è i parametri desiderati per il controller 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;

Dopo aver aperto con successo SPI_Handle, gli sviluppatori possono avviare immediatamente transazioni SPI. Ogni transazione SPI è descritta utilizzando la struttura dati - 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;

Ad esempio, per avviare una transazione di scrittura sul bus SPI, gli sviluppatori devono preparare un 'txBuf' pieno di dati da trasmettere e impostare la variabile 'conteggio' sulla lunghezza dei byte di dati da inviare. Infine, una chiamata a SPI_transfer (spiHandle, spiTrans) segnala al controller SPI di avviare la transazione.

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);
}

Poiché SPI è un protocollo duplex che trasmette e riceve allo stesso tempo, quando una transazione di scrittura è terminata, i relativi dati di risposta sono già disponibili su "rxBuf".

Poiché impostiamo la modalità di trasferimento sulla modalità di richiamata, ogni volta che una transazione viene completata, viene richiamata la funzione di richiamata registrata. Qui è dove gestiamo i dati di risposta o iniziamo la transazione successiva. (Nota: ricorda sempre di non fare più delle necessarie chiamate API all'interno di una funzione di callback).

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);
}

Configurazione pin I / O

Fino ad ora, sembra abbastanza semplice usare il driver SPI. Ma aspetta, come è possibile connettere le chiamate API del software ai segnali SPI fisici? Questo viene fatto attraverso tre strutture dati: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 e SPI_Config. Normalmente vengono istanziati in una posizione diversa come "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}
};

L'array SPI_Config ha una voce separata per ogni controller SPI hardware. Ogni voce ha tre campi: fxnTablePtr, object e hwAttrs. 'FxnTablePtr' è una tabella di punti che punta alle implementazioni specifiche del dispositivo dell'API del driver.

L '"oggetto" tiene traccia delle informazioni come stato del driver, modalità di trasferimento, funzione di callback per il conducente. Questo "oggetto" viene gestito automaticamente dal conducente.

'HwAttrs' memorizza i dati di mappatura delle risorse hardware effettivi, ad es. I pin IO per i segnali SPI, il numero di interruzione hardware, l'indirizzo base del controller SPI ecc. La maggior parte dei campi 'hwAttrs' sono predefiniti e non possono essere modificati. Mentre i pin IO dell'interfaccia possono essere liberamente assegnati ai casi utente. Nota: gli MCU CC26XX scollegano i pin IO da specifiche funzionalità periferiche che uno qualsiasi dei pin IO può essere assegnato a qualsiasi funzione periferica.

Ovviamente i pin IO effettivi devono essere definiti prima nel "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

Di conseguenza, dopo la configurazione della mappatura delle risorse hardware, gli sviluppatori possono finalmente comunicare con i chip dei sensori esterni tramite l'interfaccia SPI.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow