Zoeken…


Verbinding maken met BLE Slave-apparaten

Invoering

SoC's uit de CC26XX- serie van Texas Instruments (TI) zijn direct beschikbare draadloze MCU's die zijn gericht op Bluetooth Low Energy (BLE) -applicaties. Samen met de MCU's biedt TI een volwaardige softwarestack die de benodigde API- en voorbeeldcodes biedt om ontwikkelaars snel aan de slag te helpen met de gereedschapsketen. Voor beginners is er echter altijd de vraag waar te beginnen voor een lange lijst met referentiedocumenten en codes. Deze notitie heeft als doel de benodigde stappen vast te leggen die nodig zijn om het eerste project op gang te brengen.

Het Simple Peripheral Profile is het 'Hello World'-voorbeeld van de BLE-stack, waarbij de MCU fungeert als een BLE-randapparaat voor upstream-hosts of BLE-serviceclients, zoals pc's en smartphones. Veel voorkomende toepassingen in de echte wereld zijn: Bluetooth-hoofdtelefoon, Bluetooth-temperatuursensor, enz.

Voordat we beginnen, moeten we eerst basissoftware- en hardwaretools verzamelen voor programmeren en debuggen.

  1. BLE-stapel

    Download en installeer TI's BLE-STACK-2-2-0 van de officiële website. Stel dat het op de standaardlocatie 'C: \ ti' is geïnstalleerd.

  2. IDE - er zijn twee opties:

  • IAR Embedded Workbench voor ARM. Dit is een commercieel hulpmiddel met een gratis evaluatieperiode van 30 dagen.

  • TI's Code Composer Studio (CCS). De officiële IDE van TI en biedt een gratis licentie. In dit voorbeeld zullen we CCS V6.1.3 gebruiken

  1. Hardware programmeer tool

    Beveel TI's XDS100 USB-interface JTAG-apparaat aan.

Voorbeeldproject importeren in CCS

De voorbeeldcode van het Simple Peripheral Profile wordt geleverd met de BLE-Stack-installatie. Volg de onderstaande stappen om dit voorbeeldproject naar CCS te importeren.

  1. Start CCS, maak een map voor de werkruimte. Vervolgens Bestand-> Importeren. Selecteer onder 'Selecteer een importbron' de optie 'Code Compose Studio -> CCS Projects' en klik op 'Next'. CCS-project importeren
  2. Blader naar 'C: \ ti \ simplelink \ ble_sdk_2_02_00_31 \ voorbeelden \ cc2650em \ simple_peripheral \ ccs'. Twee projecten zullen worden ontdekt. Selecteer alles en vink beide onderstaande opties aan. Klik vervolgens op 'Voltooien'. Door projecten naar de werkruimte te kopiëren, blijft de oorspronkelijke projectinstelling ongewijzigd voor alle volgende wijzigingen. voer hier de afbeeldingsbeschrijving in

Het voorbeeld van het eenvoudige perifere profiel omvat twee projecten:

  • simple_peripheral_cc2650em_app
  • simple_peripheral_cc2650em_stack

'cc2650em' is de codenaam voor het CC2650-evaluatiebord van TI. Het _stack-project omvat de codes en de binaire code van TI's BEL-Stack-2-2-0, die de Bluetooth-advertenties, handshaking, frequentiesynchronisatie enz. Afhandelt. Dit is het deel van de code dat relatief stabiel is en niet wil zijn meestal aangeraakt door ontwikkelaars. In het _app-project implementeren ontwikkelaars hun eigen taken en BLE-service.

Bouwen en downloaden

Klik op menu's 'Project-> Alles bouwen' om beide projecten te bouwen. Als de compiler een soort interne fout bij het koppelen rapporteert, probeer dan de 'compress_dwarf' optie voor de linker uit te schakelen door:

  • klik met de rechtermuisknop op het project en selecteer 'Propoerties'.
  • Klik in 'Build-> ARM Linker' op de knop 'Vlaggen bewerken'.
  • wijzig de laatste optie in '--compress_dwarf = off'.

Nadat beide projecten met succes zijn gebouwd, klikt u afzonderlijk op 'Uitvoeren> Foutopsporing' om zowel de stapel- als app-afbeeldingen naar de MCU te downloaden.

Raak de code aan

Om agressieve wijzigingen in de voorbeeldcode aan te brengen, moeten ontwikkelaars gedetailleerde kennis verkrijgen over de gelaagde structuur van BLE-stack. Voor elementaire taken zoals temperatuuraflezing / melding kunnen we ons op slechts twee bestanden concentreren: PROFILES / simple_gatt_profile.c (.h) en Application / simple_peripheral.c (.h)

simple_gatt_profile.c

Alle Bluetooth-applicaties bieden een bepaald type service, elk bestaat uit een set kenmerken. Het eenvoudige randprofiel definieert één eenvoudige service, met de UUID van 0xFFF0, die uit 5 kenmerken bestaat. Deze service is gespecificeerd in simple_gatt_profile.c. Een samenvatting van de eenvoudige service wordt als volgt weergegeven.

Naam Gegevensgrootte UUID Beschrijving Eigendom
simplePeripheralChar1 1 0xFFF1 Kenmerken 1 Lezen schrijven
simplePeripheralChar2 1 0xFFF2 Kenmerken 2 Alleen lezen
simplePeripheralChar3 1 0xFFF3 Kenmerken 3 Alleen schrijven
simplePeripheralChar4 1 0xFFF4 Kenmerken 4 verwittigen
simplePeripheralChar5 5 0xFFF5 Kenmerken 5 Alleen lezen

De vijf kenmerken hebben verschillende eigenschappen en dienen als voorbeelden voor verschillende gebruikersgevallen. De MCU kan bijvoorbeeld simplePeripheralChar4 gebruiken om zijn klanten, upstream-hosts, op de hoogte te stellen van de wijziging van informatie.

Om een Bluetooth-service te definiëren, moet men een attributentabel maken.

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

De attributentabel begint met een standaard 'primaryServiceUUID', die de UUID van de service aangeeft (in dit geval 0xFFF0). Daarna volgen verklaringen van alle kenmerken waaruit de dienst bestaat. Elke karakteristiek heeft verschillende attributen, namelijk toegangsmachtiging, waarde en gebruikersbeschrijving, enz. Deze tabel wordt later geregistreerd bij de BLE-stapel.

// Register GATT attribute list and CBs with GATT Server App
status = GATTServApp_RegisterService( simpleProfileAttrTbl, 
                                      GATT_NUM_ATTRS( simpleProfileAttrTbl ),
                                      GATT_MAX_ENCRYPT_KEY_SIZE,
                                      &simpleProfileCBs );

Bij registratie van de service moeten ontwikkelaars drie callback-functies bieden voor 'Lezen', 'Schrijven' en 'Autorisatie' van de kenmerken. We kunnen in de voorbeeldcode de lijst met terugbelfuncties vinden.

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

Dus SimpleProfile_ReadAttrCB wordt aangeroepen zodra de serviceclient een leesverzoek verzendt via de Bluetooth-verbinding. Evenzo wordt simpleProfile_WriteAttrCB aangeroepen wanneer een schrijfverzoek wordt gedaan. Het begrijpen van deze twee functies is de sleutel tot het succes van projectaanpassing.

Hieronder vindt u de functie Terugbellen terugbellen.

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

Ik heb de code enigszins aangepast van de originele versie. Voor deze functie zijn 7 parameters nodig, die worden uitgelegd in de koptekstcommentaar. De functie begint met het controleren van de toegangsmachtiging van het attribuut, bijvoorbeeld of het leesrechten heeft. Vervolgens wordt gecontroleerd of dit een segmentuitlezing is van een groter blob-leesverzoek door de voorwaarde 'if (offset> 0)' te testen. Uiteraard ondersteunt de functie momenteel geen blob read. Vervolgens wordt de UUID van het gevraagde kenmerk geëxtraheerd. Er zijn twee soorten UUID: 16-bit en 128-bit. Terwijl de voorbeeldcode alle kenmerken definieert met behulp van 16-bits UUID's, is de 128-bits UUID universeler en vaker gebruikt in upstream-hosts zoals pc's en smartphones. Daarom worden verschillende coderegels gebruikt om 128-bits UUID te converteren naar 16-bits 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]);

Nadat we de UUID hebben verkregen, kunnen we uiteindelijk bepalen welk kenmerk wordt aangevraagd. De resterende taak aan de zijde van de ontwikkelaar is dan om de waarde van het gevraagde kenmerk te kopiëren naar de bestemmingsaanwijzer '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;
    }

De terugbelfunctie voor schrijven is vergelijkbaar, behalve dat er een speciaal type schrijven is met UUID van GATT_CLIENT_CHAR_CFG_UUID. Dit is het verzoek van de upstream-host om zich te registreren voor kennisgeving van kenmerken of indicatie. Roep eenvoudig de API GATTServApp_ProcessCCCWriteReq aan om het verzoek door te geven aan de BLE-stapel.

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;

De toepassingszijde van de code op de MCU wil mogelijk op de hoogte worden gebracht van elke wijziging in kenmerken die tegen schrijven zijn toegestaan. Ontwikkelaars kunnen deze melding naar eigen inzicht implementeren. In de voorbeeldcode wordt de terugbelfunctie gebruikt.

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

Aan de andere kant, als de BLE-randapparatuur upstream hosts op de hoogte wil brengen van elke wijziging in zijn kenmerk, kan hij de API GATTServApp_ProcessCharCfg aanroepen. Deze API wordt gedemonstreerd in de functie 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 );
}

Dus als de eenvoudige randapplicatie de huidige waarde van SIMPLEPROFILE_CHAR4 aan peer-apparaten wil melden, kan deze eenvoudig de functie SimpleProfile_SetParameter aanroepen.

Samenvattend definieert PROFILES / simple_gatt_profile.c (.h) de inhoud van de service die de BLE-randapparatuur aan zijn klanten wil aanbieden, evenals de manieren waarop die kenmerken in de service worden benaderd.

simple_peripheral.c

De BLE-stapel van TI draait op een kleine OS-laag met meerdere threads. Om een werkbelasting aan de MCU toe te voegen, moeten ontwikkelaars eerst een taak maken. simple_peripheral.c demonstreert de basisstructuur van een aangepaste taak, inclusief het maken, initialiseren en beheren van de taak. Om te beginnen met de zeer basistaken zoals temperatuurmeting en melding, zullen we ons hieronder concentreren op een paar belangrijke functies.

Het begin van het bestand definieert een set parameters die het gedrag van de Bluetooth-verbinding kunnen beïnvloeden.

// 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 en DEFAULT_DESIRED_SLAVE_LATENCY definiëren samen het verbindingsinterval van een Bluetooth-verbinding, wat is hoe vaak een paar apparaten informatie uitwisselen. Een lager verbindingsinterval betekent een responsiever gedrag maar ook een hoger stroomverbruik.

Parameter DEFAULT_DESIRED_CONN_TIMEOUT bepaalt hoe lang een peer-reactie wordt ontvangen voordat een verbinding als verloren wordt beschouwd. Parameter DEFAULT_ENABLE_UPDATE_REQUEST bepaalt of het slave-apparaat het verbindingsinterval tijdens runtime mag wijzigen. Voor energiebesparing is het handig om verschillende verbindingsparameters te hebben voor drukke en inactieve fasen.

De parameter SBP_PERIODIC_EVT_PERIOD definieert de periode van een klokgebeurtenis waarmee de taak periodiek een functie-aanroep uitvoert. Dit is de perfecte plaats voor ons om de code toe te voegen om de temperatuur te lezen en de servicecliënten op de hoogte te stellen.

De periodieke klok wordt geïnitieerd in de functie SimpleBLEPeripheral_init.

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

Hiermee maakt u een klok met een periode van SBP_PERIODIC_EVT_PERIOD. En roept bij time-out de functie SimpleBLEPeripheral_clockHandler aan met parameter SBP_PERIODIC_EVT. De klokgebeurtenis kan vervolgens worden geactiveerd door

Util_startClock(&periodicClock);

Zoeken naar het trefwoord Util_startClock, kunnen we vaststellen dat deze periodieke klok eerst wordt geactiveerd bij de gebeurtenis GAPROLE_CONNECTED (in de functie SimpleBLEPeripheral_processStateChangeEvt), wat betekent dat de taak een periodieke routine start zodra deze verbinding met een host tot stand brengt.

Wanneer de periodieke klok afloopt, wordt de geregistreerde callback-functie opgeroepen.

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

Met deze functie wordt een vlag in de gebeurtenissenvector ingesteld en wordt de toepassing vanuit de OS-takenlijst geactiveerd. Merk op dat we geen specifieke gebruikersworkload uitvoeren in deze callback-functie, omdat dit NIET wordt aanbevolen. De werklast van gebruikers omvat vaak oproepen naar API's van de BLE-stack. Het doen van BLE-stack API-aanroepen binnen een callback-functie leidt vaak tot systeemuitzonderingen. In plaats daarvan plaatsen we een vlag in de gebeurtenissenvector van de taak en wachten we dat deze later in de toepassingscontext wordt verwerkt. Het beginpunt voor de voorbeeldtaak 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();
    }

  }
}

Het is een oneindige lus die de stapel van de taak en de wachtrijen van toepassingsberichten blijft opvragen. Het controleert ook de gebeurtenissenvector op verschillende vlaggen. Dat is waar de periodieke routine daadwerkelijk wordt uitgevoerd. Bij ontdekking van een SBP_PERIODIC_EVT wist de taakfunctie eerst de vlag, start onmiddellijk dezelfde timer en roept de routine-functie SimpleBLEPeripheral_performPeriodicTask () aan;

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

Binnen de periodieke functie voeren we onze zeer specifieke taak uit van het lezen van de temperatuur, het genereren van UART-verzoeken enz. Dan roepen we de API SimpleProfile_SetParameter () aan om de informatie via een Bluetooth-verbinding aan servicecliënten te communiceren. De BLE-stack zorgt voor alle taken op laag niveau, van het onderhouden van de draadloze verbinding tot het verzenden van berichten via de Bluetooth-link. Het enige dat ontwikkelaars hoeven te doen, is om de applicatiespecifieke gegevens te verzamelen en bij te werken naar overeenkomstige kenmerken in een servicetabel.

Ten slotte zal, wanneer een schrijfverzoek wordt uitgevoerd op een schrijf-toegestane karakteristieken, een callback-functie worden opgeroepen.

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

Nogmaals, deze callback-functie haalt alleen een toepassingsbericht op voor de gebruikerstaak, dat later in de toepassingscontext wordt afgehandeld.

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

Wanneer in het bovenstaande voorbeeld SIMPLEPROFILE_CHAR1 is geschreven, haalt de gebruikerscode eerst de nieuwe waarde op door SimpleProfile_GetParameter () aan te roepen en vervolgens de gegevens te parseren voor door de gebruiker gedefinieerde opdrachten.

Samenvattend toont simple_peripheral.c een voorbeeld van hoe een gebruikerstaak voor aangepaste workloads te maken. Een eenvoudige manier om de werkbelasting van een applicatie te plannen is door een periodieke klokgebeurtenis. Ontwikkelaars hoeven alleen informatie van / naar de kenmerken in de servicetabel te verwerken, terwijl de BLE-stack zorgt voor de rest van de communicatie van de informatie uit de servicetabel naar peer-apparaten (of vice versa) via Bluetooth-verbinding.

Sensoren uit de echte wereld verbinden

Om BLE-slave-apparaten nuttig te laten werken, zijn de GPIO's van de draadloze MCU bijna altijd betrokken. Om bijvoorbeeld de temperatuur van een externe sensor te lezen, kan de ADC-functionaliteit van GPIO-pinnen vereist zijn. TI's CC2640 MCU beschikt over maximaal 31 GPIO's, gegeven verschillende verpakkingstypen.

Aan de hardwarezijde biedt CC2640 een uitgebreide set randfuncties zoals ADC, UARTS, SPI, SSI, I2C enz. Aan de softwarezijde probeert de BLE-stack van TI een uniforme apparaatonafhankelijke driverinterface voor verschillende randapparatuur te bieden. Een uniforme stuurprogramma-interface kan de kans op herbruikbaarheid van code verbeteren, maar aan de andere kant verhoogt het ook de helling van de leercurve. In deze notitie gebruiken we de SPI-controller als voorbeeld en laten we zien hoe het softwarestuurprogramma in gebruikerstoepassingen kan worden geïntegreerd.

Basic SPI Driver Flow

In de BLE-stack van TI bestaat een randstuurprogramma vaak uit drie delen: een apparaatonafhankelijke specificatie van de stuurprogramma-API's; een apparaatspecifieke implementatie van de stuurprogramma-API's en een toewijzing van hardwarebronnen.

Voor de SPI-controller omvat de implementatie van het stuurprogramma drie bestanden:

  • <ti / drivers / SPI.h> - dit is de apparaatonafhankelijke API-specificatie
  • <ti / drivers / spi / SPICC26XXDMA.h> - dit is de CC2640-specifieke API-implementatie
  • <ti / drivers / dma / UDMACC26XX.h> - dit is het uDMA-stuurprogramma vereist door het SPI-stuurprogramma

(Opmerking: het beste document voor de perifere stuurprogramma's van de BLE-stack van TI is meestal te vinden in hun header-bestanden, zoals SPICC26XXDMA.h in dit geval)

Om de SPI-controller te gaan gebruiken, maken we eerst een aangepast c-bestand, namelijk sbp_spi.c, dat de drie headerbestanden hierboven bevat. De natuurlijke volgende stap is om een instantie van de bestuurder te maken en deze te starten. De driverinstantie is ingekapseld in de gegevensstructuur - SPI_Handle. Een andere gegevensstructuur - SPI_Params wordt gebruikt om de belangrijkste parameters voor de SPI-controller op te geven, zoals bitsnelheid, overdrachtsmodus, enz.

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

De bovenstaande voorbeeldcode illustreert hoe de SPI_Handle-instantie moet worden geïnitialiseerd. De API SPI_init () moet eerst worden aangeroepen om interne gegevensstructuren te initialiseren. De functieaanroep SPI_Params_init (& spiParams) stelt alle velden van de SPI_Params-structuur in op standaardwaarden. Vervolgens kunnen ontwikkelaars sleutelparameters aanpassen aan hun specifieke gevallen. De bovenstaande code stelt bijvoorbeeld de SPI-controller in om in de master-modus te werken met een bitsnelheid van 800 kbps en gebruikt een niet-blokkerende methode om elke transactie te verwerken, zodat wanneer een transactie is voltooid de callback-functie sbp_spiCallback wordt aangeroepen.

Ten slotte opent een aanroep van SPI_open () de hardware SPI-controller en retourneert een handle voor latere SPI-transacties. De SPI_open () heeft twee argumenten, de eerste is de ID van de SPI-controller. CC2640 beschikt over twee hardware SPI-controllers op de chip, dus deze ID-argumenten zijn 0 of 1 zoals hieronder gedefinieerd. Het tweede argument zijn de gewenste parameters voor de SPI-controller.

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

Na het succesvol openen van de SPI_Handle kunnen ontwikkelaars onmiddellijk SPI-transacties initiëren. Elke SPI-transactie wordt beschreven met behulp van de gegevensstructuur - 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;

Om bijvoorbeeld een schrijftransactie op de SPI-bus te starten, moeten ontwikkelaars een 'txBuf' voorbereiden die gevuld is met te verzenden gegevens en de variabele 'count' instellen op de lengte van te verzenden databytes. Ten slotte geeft een oproep aan de SPI_transfer (spiHandle, spiTrans) de SPI-controller aan om de transactie te starten.

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

Omdat SPI een duplexprotocol is dat zowel verzenden als ontvangen tegelijkertijd gebeurt, zijn de bijbehorende antwoordgegevens al beschikbaar op de 'rxBuf' wanneer een schrijftransactie is voltooid.

Omdat we de overdrachtsmodus op terugbelmodus instellen, wordt de geregistreerde terugbelfunctie opgeroepen wanneer een transactie is voltooid. Dit is waar we de responsgegevens verwerken of de volgende transactie initiëren. (Opmerking: vergeet niet om niet meer dan de benodigde API-aanroepen te doen binnen een callback-functie).

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

I / O Pin-configuratie

Tot nu toe lijkt het redelijk eenvoudig om het SPI-stuurprogramma te gebruiken. Maar wacht, hoe kunnen de software-API-aanroepen worden verbonden met fysieke SPI-signalen? Dit gebeurt via drie gegevensstructuren: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 en SPI_Config. Ze worden normaal gesproken op een andere locatie gestart, zoals '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}
};

De SPI_Config-array heeft een afzonderlijke vermelding voor elke hardware SPI-controller. Elk item heeft drie velden: fxnTablePtr, object en hwAttrs. De 'fxnTablePtr' is een puntentabel die verwijst naar de apparaatspecifieke implementaties van de stuurprogramma-API.

Het 'object' houdt informatie bij, zoals de status van de bestuurder, de overdrachtsmodus en de terugbelfunctie voor de bestuurder. Dit 'object' wordt automatisch onderhouden door de bestuurder.

De 'hwAttrs' slaat de actuele hardware resource mapping-gegevens op, bijv. De IO-pins voor de SPI-signalen, het hardware-onderbrekingsnummer, het basisadres van de SPI-controller enz. De meeste velden van de 'hwAttrs' zijn vooraf gedefinieerd en kunnen niet worden gewijzigd. Terwijl de IO-pinnen van de interface vrijelijk op gebruikers gebaseerde cases kunnen worden toegewezen. Opmerking: de CC26XX MCU's ontkoppelen de IO-pinnen van specifieke randfunctionaliteit die aan elk van de IO-pinnen kan worden toegewezen aan elke randfunctie.

Natuurlijk moeten de werkelijke IO-pinnen eerst in het 'board.h' worden gedefinieerd.

#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

Als gevolg hiervan kunnen ontwikkelaars na configuratie van hardware resource mapping eindelijk communiceren met externe sensorchips via de SPI-interface.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow