bluetooth
Beginnen Sie mit dem BLE-Stack von TI
Suche…
Anschließen an BLE-Slave-Geräte
Einführung
Die SoCs der CC26XX- Serie von Texas Instruments (TI) sind schnell verfügbare drahtlose MCUs für Bluetooth-Low-Energy-Anwendungen (BLE). Neben den MCUs bietet TI einen vollwertigen Software-Stack , der die erforderlichen API- und Beispielcodes bereitstellt, um Entwicklern einen schnellen Einstieg in die Toolkette zu ermöglichen. Für Anfänger stellt sich jedoch immer die Frage, wo man vor einer langen Liste von Referenzdokumenten und Codes beginnen muss. Diese Notiz soll die notwendigen Schritte aufzeichnen, die erforderlich sind, um das erste Projekt in Gang zu setzen.
Das Simple Peripheral Profile ist das 'Hello World'-Beispiel des BLE-Stacks, bei dem die MCU als BLE-Peripheriegerät für Upstream-Hosts oder BLE-Service-Clients wie PC und Smartphones fungiert. Übliche reale Anwendungen umfassen: Bluetooth-Kopfhörer, Bluetooth-Temperatursensor usw.
Vor dem Start müssen wir zunächst grundlegende Software- und Hardware-Tools zum Programmieren und Debuggen zusammenstellen.
BLE-Stapel
Laden Sie das BLE-STACK-2-2-0 von TI von der offiziellen Website herunter und installieren Sie es. Angenommen, es ist am Standardspeicherort 'C: \ ti' installiert.
IDE - es gibt zwei Möglichkeiten:
IAR Embedded Workbench für ARM. Dies ist ein kommerzielles Tool mit einer 30-tägigen Testphase.
Code Composer Studio (CCS) von TI. Die offizielle IDE von TI bietet eine kostenlose Lizenz. In diesem Beispiel verwenden wir CCS V6.1.3
Hardware-Programmierwerkzeug
Empfehlen Sie das XDS100 USB-Interface-JTAG-Gerät von TI.
Beispielprojekt in CCS importieren
Der Beispielcode "Simple Peripheral Profile" wird mit der Installation von BLE-Stack geliefert. Führen Sie die folgenden Schritte aus, um dieses Beispielprojekt in CCS zu importieren.
- Starten Sie CCS und erstellen Sie einen Arbeitsbereichsordner. Dann Datei-> Importieren. Wählen Sie unter 'Importquelle auswählen' die Option 'Code Compose Studio -> CCS-Projekte' aus und klicken Sie auf 'Weiter'.
- Wechseln Sie zu "C: \ ti \ simplelink \ ble_sdk_2_02_00_31 \ Examples \ cc2650em \ simple_peripheral \ ccs". Zwei Projekte werden entdeckt. Wählen Sie alle aus und aktivieren Sie beide Optionen. Klicken Sie anschließend auf "Fertig stellen". Durch das Kopieren von Projekten in den Arbeitsbereich lassen Sie die ursprüngliche Projekteinstellung für alle folgenden Änderungen unverändert.
Das Beispiel für ein einfaches Peripherieprofil umfasst zwei Projekte:
- simple_peripheral_cc2650em_app
- simple_peripheral_cc2650em_stack
'cc2650em' ist der Codename für das cc2650-Evaluierungsboard von TI. Das _stack-Projekt enthält die Codes und die Binärdatei von BEL-Stack-2-2-0 von TI, das für die Bluetooth-Werbung, das Handshaking, die Frequenzsynchronisation usw. zuständig ist. Dies ist der Teil des Codes, der relativ stabil ist und nicht sein soll die meiste Zeit von Entwicklern berührt. Im _app-Projekt implementieren Entwickler ihre eigenen Aufgaben und BLE-Dienste.
Erstellen und herunterladen
Klicken Sie auf die Menüs 'Projekt-> Alle erstellen', um beide Projekte zu erstellen. Wenn der Compiler einen internen Fehler beim Verknüpfen meldet, deaktivieren Sie die Option 'compress_dwarf' für den Linker folgendermaßen:
- Klicken Sie mit der rechten Maustaste auf das Projekt und wählen Sie 'Eigenschaften'.
- Klicken Sie unter 'Build-> ARM Linker' auf die Schaltfläche 'Flags bearbeiten'.
- Ändern Sie die letzte Option in '--compress_dwarf = off'.
Nachdem beide Projekte erfolgreich erstellt wurden, klicken Sie separat auf "Ausführen -> Debuggen", um sowohl den Stack als auch die App-Images auf die MCU herunterzuladen.
Tippen Sie auf den Code
Um aggressive Änderungen am Beispielcode vornehmen zu können, müssen Entwickler detaillierte Kenntnisse über die mehrschichtige Struktur des BLE-Stapels erwerben. Für grundlegende Aufgaben wie Temperaturablesung / Benachrichtigung können wir uns nur auf zwei Dateien konzentrieren: PROFILES / simple_gatt_profile.c (.h) und Application / simple_peripheral.c (.h)
simple_gatt_profile.c
Alle Bluetooth-Anwendungen bieten eine bestimmte Art von Diensten an, wobei jede aus einer Reihe von Eigenschaften besteht. Das einfache Peripherieprofil definiert einen einfachen Dienst mit der UUID 0xFFF0, die aus 5 Merkmalen besteht. Dieser Dienst wird in simple_gatt_profile.c angegeben. Eine Zusammenfassung des einfachen Dienstes wird wie folgt aufgelistet.
Name | Datengröße | UUID | Beschreibung | Eigentum |
---|---|---|---|---|
simplePeripheralChar1 | 1 | 0xFFF1 | Eigenschaften 1 | Lesen Schreiben |
simplePeripheralChar2 | 1 | 0xFFF2 | Eigenschaften 2 | Schreibgeschützt |
simplePeripheralChar3 | 1 | 0xFFF3 | Eigenschaften 3 | Schreibe nur |
simplePeripheralChar4 | 1 | 0xFFF4 | Eigenschaften 4 | Benachrichtigen |
simplePeripheralChar5 | 5 | 0xFFF5 | Eigenschaften 5 | Schreibgeschützt |
Die fünf Merkmale haben unterschiedliche Eigenschaften und dienen als Beispiel für verschiedene Anwenderfälle. Zum Beispiel kann die MCU simplePeripheralChar4 verwenden, um ihre Clients (Upstream-Hosts) über die Informationsänderung zu informieren.
Um einen Bluetooth-Dienst zu definieren, muss eine Attributtabelle erstellt werden.
/*********************************************************************
* 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
},
...
};
Die Attributtabelle beginnt mit einer Standardeinstellung 'primaryServiceUUID', die die UUID des Dienstes angibt (in diesem Fall 0xFFF0). Es folgen dann Deklarationen aller Merkmale, aus denen der Dienst besteht. Jedes Merkmal hat mehrere Attribute, nämlich Zugriffsberechtigung, Wert und Benutzerbeschreibung usw. Diese Tabelle wird später beim BLE-Stack registriert.
// Register GATT attribute list and CBs with GATT Server App
status = GATTServApp_RegisterService( simpleProfileAttrTbl,
GATT_NUM_ATTRS( simpleProfileAttrTbl ),
GATT_MAX_ENCRYPT_KEY_SIZE,
&simpleProfileCBs );
Bei der Registrierung des Dienstes müssen Entwickler drei Rückruffunktionen für 'Lesen', 'Schreiben' und 'Autorisierung' der Merkmale bereitstellen. Im Beispielcode finden Sie die Liste der Rückruffunktionen.
/*********************************************************************
* PROFILE CALLBACKS
*/
// Simple Profile Service Callbacks
// Note: When an operation on a characteristic requires authorization and
// pfnAuthorizeAttrCB is not defined for that characteristic's service, the
// Stack will report a status of ATT_ERR_UNLIKELY to the client. When an
// operation on a characteristic requires authorization the Stack will call
// pfnAuthorizeAttrCB to check a client's authorization prior to calling
// pfnReadAttrCB or pfnWriteAttrCB, so no checks for authorization need to be
// made within these functions.
CONST gattServiceCBs_t simpleProfileCBs =
{
simpleProfile_ReadAttrCB, // Read callback function pointer
simpleProfile_WriteAttrCB, // Write callback function pointer
NULL // Authorization callback function pointer
};
So wird simpleProfile_ReadAttrCB aufgerufen, sobald der Service-Client eine Leseanforderung über die Bluetooth-Verbindung sendet. In ähnlicher Weise wird simpleProfile_WriteAttrCB aufgerufen, wenn eine Schreibanforderung gestellt wird. Das Verständnis dieser beiden Funktionen ist der Schlüssel zum Erfolg der Projektanpassung.
Nachfolgend finden Sie die Read Callback-Funktion.
/*********************************************************************
* @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 );
}
Ich habe den Code gegenüber der Originalversion leicht modifiziert. Diese Funktion benötigt 7 Parameter, die in den Kopfkommentaren erläutert werden. Die Funktion beginnt mit der Überprüfung der Zugriffsberechtigung des Attributs, z. B. ob es über Leseberechtigung verfügt. Dann wird geprüft, ob dies ein Segmentlesen einer größeren Blob-Leseanforderung ist, indem die Bedingung 'if (offset> 0)' getestet wird. Offensichtlich unterstützt die Funktion momentan kein Blob-Lesen. Als nächstes wird die UUID des angeforderten Attributs extrahiert. Es gibt zwei Arten von UUIDs: 16 Bit und 128 Bit. Während der Beispielcode alle Eigenschaften mit 16-Bit-UUIDs definiert, ist die 128-Bit-UUID universeller und wird häufiger in Upstream-Hosts wie PCs und Smartphones verwendet. Daher werden mehrere Codezeilen verwendet, um 128-Bit-UUID in 16-Bit-UUID umzuwandeln.
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]);
Nachdem wir die UUID abgerufen haben, können wir schließlich bestimmen, welches Attribut angefordert wird. Die verbleibende Aufgabe auf der Entwicklerseite besteht dann darin, den Wert des angeforderten Attributs in den Zielzeiger 'pValue' zu kopieren.
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;
}
Die Write-Callback-Funktion ist ähnlich, mit der Ausnahme, dass es eine spezielle Art von Write mit der UUID von GATT_CLIENT_CHAR_CFG_UUID gibt. Dies ist die Aufforderung des vorgelagerten Hosts, sich für die Benachrichtigung oder Anzeige von Eigenschaften zu registrieren. Rufen Sie einfach die API GATTServApp_ProcessCCCWriteReq auf, um die Anforderung an den BLE-Stack zu übergeben.
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;
Die Anwendungsseite des Codes auf der MCU möchte möglicherweise über Änderungen an schreibzulässigen Eigenschaften informiert werden. Entwickler können diese Benachrichtigung beliebig implementieren. Im Beispielcode wird die Callback-Funktion verwendet.
// 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 );
}
Wenn das BLE-Peripheriegerät jedoch Upstream-Hosts über jede Änderung seines Merkmals benachrichtigen möchte, kann es die API GATTServApp_ProcessCharCfg aufrufen. Diese API wird in der Funktion SimpleProfile_SetParameter demonstriert.
/*********************************************************************
* @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 );
}
Wenn also die einfache Peripherieanwendung den aktuellen Wert von SIMPLEPROFILE_CHAR4 an Peer-Geräte benachrichtigen möchte, kann sie einfach die SimpleProfile_SetParameter-Funktion aufrufen.
Zusammenfassend definiert PROFILES / simple_gatt_profile.c (.h) den Inhalt des Dienstes, den das BLE-Peripheriegerät seinen Kunden anbieten möchte, sowie die Art und Weise, wie auf diese Merkmale des Dienstes zugegriffen wird.
simple_peripheral.c
Der BLE-Stack von TI läuft auf einer Lite-Multithread-OS-Schicht. Um der MCU einen Workload hinzuzufügen, müssen Entwickler zunächst eine Aufgabe erstellen. simple_peripheral.c demonstriert die grundlegende Struktur einer benutzerdefinierten Aufgabe, die das Erstellen, Initialisieren und Verwalten der Aufgabe umfasst. Um mit den grundlegenden Aufgaben wie Temperaturablesung und Benachrichtigung zu beginnen, werden wir uns auf einige wichtige Funktionen konzentrieren.
Der Anfang der Datei definiert eine Reihe von Parametern, die das Verhalten der Bluetooth-Verbindung beeinflussen können.
// 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
Die Parameter DEFAULT_DESIRED_MIN_CONN_INTERVAL, DEFAULT_DESIRED_MAX_CONN_INTERVAL und DEFAULT_DESIRED_SLAVE_LATENCY definieren zusammen das Verbindungsintervall einer Bluetooth-Verbindung. Dabei wird angegeben, wie häufig ein Gerätepaar Informationen austauscht. Ein niedrigeres Verbindungsintervall bedeutet ein besseres Ansprechverhalten, aber auch einen höheren Stromverbrauch.
Der Parameter DEFAULT_DESIRED_CONN_TIMEOUT legt fest, wie lange eine Peer-Antwort empfangen wird, bevor eine Verbindung als unterbrochen gilt. Der Parameter DEFAULT_ENABLE_UPDATE_REQUEST legt fest, ob das Slave-Gerät das Verbindungsintervall während der Laufzeit ändern darf. Im Hinblick auf die Energieeinsparung ist es nützlich, unterschiedliche Verbindungsparameter für die Beschäftigungs- und Ruhephasen zu haben.
Der Parameter SBP_PERIODIC_EVT_PERIOD definiert den Zeitraum eines Uhrzeitereignisses, bei dem die Task einen Funktionsaufruf periodisch ausführt. Dies ist der perfekte Ort für uns, um den Code zum Lesen der Temperatur hinzuzufügen und die Service-Kunden zu benachrichtigen.
Die periodische Uhr wird in der Funktion SimpleBLEPeripheral_init gestartet.
// Create one-shot clocks for internal periodic events.
Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);
Dadurch wird eine Uhr mit einer Periode von SBP_PERIODIC_EVT_PERIOD erstellt. Bei einem Timeout wird die Funktion von SimpleBLEPeripheral_clockHandler mit dem Parameter SBP_PERIODIC_EVT aufgerufen. Das Uhrzeitereignis kann dann durch ausgelöst werden
Util_startClock(&periodicClock);
Bei der Suche nach dem Schlüsselwort Util_startClock können wir feststellen, dass diese periodische Uhr zuerst beim Ereignis GAPROLE_CONNECTED (innerhalb der Funktion SimpleBLEPeripheral_processStateChangeEvt) ausgelöst wird. Dies bedeutet, dass die Task eine periodische Routine startet, sobald eine Verbindung mit einem Host hergestellt wird.
Wenn die periodische Uhr abläuft, wird ihre registrierte Rückruffunktion aufgerufen.
/*********************************************************************
* @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);
}
Diese Funktion setzt ein Kennzeichen im Ereignisvektor und aktiviert die Anwendung aus der OS-Aufgabenliste. Beachten Sie, dass wir in dieser Callback-Funktion keine bestimmte Benutzer-Workload durchführen, da dies NICHT empfohlen wird. Benutzer-Workload umfasst häufig Aufrufe an BLE-Stack-APIs. Das Ausführen von BLE-Stack-API-Aufrufen innerhalb einer Rückruffunktion führt häufig zu Systemausnahmen. Stattdessen setzen wir im Ereignisvektor der Task ein Flag und warten darauf, dass sie später im Anwendungskontext verarbeitet wird. Der Einstiegspunkt für die Beispielaufgabe ist 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 ist eine Endlosschleife, die ständig den Stack und die Anwendungsnachrichtenwarteschlangen der Task abfragt. Es prüft auch seinen Ereignisvektor auf verschiedene Flags. Dort wird die periodische Routine tatsächlich ausgeführt. Beim Erkennen eines SBP_PERIODIC_EVT löscht die Task-Funktion zuerst das Flag, startet sofort den gleichen Timer und ruft die Routine-Funktion SimpleBLEPeripheral_performPeriodicTask () auf;
/*********************************************************************
* @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);
}
Innerhalb der periodischen Funktion führen wir unsere sehr spezifische Aufgabe aus, die Temperatur zu lesen, UART-Anforderungen usw. zu generieren. Dann rufen wir die SimpleProfile_SetParameter () - API auf, um die Informationen über Bluetooth-Verbindung an Service-Clients zu übermitteln. Der BLE-Stack kümmert sich um alle Low-Level-Jobs, von der Aufrechterhaltung der drahtlosen Verbindung bis zur Übertragung von Nachrichten über die Bluetooth-Verbindung. Entwickler müssen lediglich die anwendungsspezifischen Daten sammeln und sie mit den entsprechenden Merkmalen in einer Servicetabelle aktualisieren.
Wenn eine Schreibanforderung für ein schreibzulässiges Merkmal ausgeführt wird, wird schließlich eine Rückruffunktion ausgelöst.
static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}
Diese Rückruffunktion reiht wiederum nur eine Anwendungsnachricht für die Benutzertask ein, die später im Anwendungskontext behandelt wird.
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;
}
}
Wenn in dem obigen Beispiel SIMPLEPROFILE_CHAR1 geschrieben wird, wird der Benutzercode zuerst den neuen Wert abrufen, indem SimpleProfile_GetParameter () aufgerufen wird, und dann die Daten für benutzerdefinierte Befehle analysiert.
Zusammenfassend zeigt die Datei simple_peripheral.c ein Beispiel für das Erstellen einer Benutzeraufgabe für benutzerdefinierte Workloads. Eine grundlegende Methode zum Planen der Anwendungsauslastung ist das periodische Uhrzeitereignis. Entwickler müssen nur Informationen zu / von den Merkmalen in der Servicetabelle verarbeiten, während der BLE-Stack den Rest der Übertragung der Informationen von der Servicetabelle an Peer-Geräte (oder umgekehrt) über eine Bluetooth-Verbindung übernimmt.
Anschließen realer Sensoren
Damit BLE-Slave-Geräte nützliche Arbeit leisten können, sind fast immer die GPIOs der drahtlosen MCU beteiligt. Um beispielsweise die Temperatur von einem externen Sensor abzulesen, kann die ADC-Funktionalität der GPIO-Pins erforderlich sein. Die CC2640 MCU von TI bietet bei verschiedenen Verpackungstypen maximal 31 GPIOs.
Auf der Hardwareseite bietet CC2640 eine Vielzahl von Peripheriefunktionen wie ADC, UARTS, SPI, SSI, I2C usw. Auf der Softwareseite versucht der BLE-Stack von TI, eine einheitliche, geräteunabhängige Treiberschnittstelle für verschiedene Peripheriegeräte bereitzustellen. Eine einheitliche Treiberschnittstelle kann die Wahrscheinlichkeit einer Wiederverwendbarkeit von Code verbessern, erhöht andererseits aber auch die Steigung der Lernkurve. In diesem Hinweis verwenden wir den SPI-Controller als Beispiel und zeigen, wie der Softwaretreiber in Benutzeranwendungen integriert wird.
Grundlegender SPI-Treiberfluss
Im BLE-Stack von TI besteht ein peripherer Treiber oft aus drei Teilen: einer geräteunabhängigen Spezifikation der Treiber-APIs; eine gerätespezifische Implementierung der Treiber-APIs und eine Zuordnung von Hardware-Ressourcen.
Für den SPI-Controller umfasst die Treiberimplementierung drei Dateien:
- <ti / drivers / SPI.h> - Dies ist die geräteunabhängige API-Spezifikation
- <ti / drivers / spi / SPICC26XXDMA.h> - Dies ist die CC2640-spezifische API-Implementierung
- <ti / drivers / dma / UDMACC26XX.h> - Dies ist der vom SPI-Treiber benötigte uDMA-Treiber
(Hinweis: Das beste Dokument für die peripheren Treiber des BLE-Stacks von TI finden Sie meistens in ihren Header-Dateien, wie in diesem Fall SPICC26XXDMA.h.)
Um mit der Verwendung des SPI-Controllers zu beginnen, erstellen Sie zuerst eine benutzerdefinierte c-Datei, nämlich sbp_spi.c, die die drei obigen Header-Dateien enthält. Der nächste natürliche Schritt ist, eine Instanz des Treibers zu erstellen und zu initiieren. Die Treiberinstanz ist in der Datenstruktur - SPI_Handle - gekapselt. Eine andere Datenstruktur - SPI_Params wird verwendet, um die Schlüsselparameter für den SPI-Controller anzugeben, wie z. B. Bitrate, Übertragungsmodus usw.
#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);
}
Der obige Beispielcode zeigt beispielhaft, wie die SPI_Handle-Instanz initialisiert wird. Die API SPI_init () muss zuerst aufgerufen werden, um interne Datenstrukturen zu initialisieren. Der Funktionsaufruf SPI_Params_init (& spiParams) setzt alle Felder der SPI_Params-Struktur auf Standardwerte. Entwickler können dann die Schlüsselparameter an ihre speziellen Fälle anpassen. Mit dem obigen Code wird der SPI-Controller beispielsweise so eingestellt, dass er im Master-Modus mit einer Bitrate von 800 KBit / s arbeitet, und verwendet eine nicht blockierende Methode, um jede Transaktion zu verarbeiten. Wenn eine Transaktion abgeschlossen ist, wird die Callback-Funktion sbp_spiCallback aufgerufen.
Schließlich wird durch einen Aufruf des SPI_open () der Hardware-SPI-Controller geöffnet und ein Handle für spätere SPI-Transaktionen zurückgegeben. Das SPI_open () hat zwei Argumente, das erste ist die ID des SPI-Controllers. CC2640 verfügt über zwei Hardware-SPI-Controller auf dem Chip. Daher sind diese ID-Argumente entweder 0 oder 1, wie unten definiert. Das zweite Argument sind die gewünschten Parameter für den 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;
Nach dem erfolgreichen Öffnen des SPI_Handle können Entwickler sofort SPI-Transaktionen initiieren. Jede SPI-Transaktion wird anhand der Datenstruktur SPI_Transaction beschrieben.
/*!
* @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;
Um beispielsweise eine Schreibtransaktion auf dem SPI-Bus zu starten, müssen Entwickler ein mit Daten zu gefülltes 'txBuf' vorbereiten, das mit den zu übertragenden Daten gefüllt ist, und die Variable 'count' auf die Länge der zu sendenden Datenbytes setzen. Ein Aufruf an den SPI_transfer (spiHandle, spiTrans) signalisiert dem SPI-Controller schließlich den Start der Transaktion.
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);
}
Da SPI ein Duplex-Protokoll ist, das gleichzeitig übertragen und empfangen wird, sind die entsprechenden Antwortdaten nach Abschluss einer Schreibtransaktion bereits in der 'rxBuf' verfügbar.
Da wir den Transfermodus auf den Rückrufmodus setzen, wird bei Abschluss einer Transaktion die registrierte Rückruffunktion aufgerufen. Hier bearbeiten wir die Antwortdaten oder initiieren die nächste Transaktion. (Hinweis: Denken Sie immer daran, nicht mehr als notwendige API-Aufrufe innerhalb einer Rückruffunktion auszuführen.)
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);
}
E / A-Pin-Konfiguration
Bis jetzt scheint es ziemlich einfach zu sein, den SPI-Treiber zu verwenden. Aber warten Sie, wie können die Software-API-Aufrufe mit physischen SPI-Signalen verbunden werden? Dies erfolgt über drei Datenstrukturen: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 und SPI_Config. Sie werden normalerweise an einem anderen Ort wie 'board.c' instanziiert.
/* 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}
};
Das SPI_Config-Array hat einen separaten Eintrag für jeden Hardware-SPI-Controller. Jeder Eintrag hat drei Felder: fxnTablePtr, object und hwAttrs. Der 'fxnTablePtr' ist eine Punktetabelle, die auf die gerätespezifischen Implementierungen der Treiber-API verweist.
Das 'Objekt' verfolgt Informationen wie Treiberstatus, Übertragungsmodus und Rückruffunktion für den Fahrer. Dieses 'Objekt' wird automatisch vom Fahrer verwaltet.
Die 'hwAttrs' speichern die tatsächlichen Hardwareressourcen-Mapping-Daten, z. B. die IO-Pins für die SPI-Signale, die Hardware-Unterbrechungsnummer, die Basisadresse des SPI-Controllers usw. Die meisten Felder der 'hwAttrs' sind vordefiniert und können nicht geändert werden. Während die IO-Pins der Schnittstelle auf Basis von Benutzerfällen frei zugeordnet werden können. Hinweis: Die CC26XX-MCUs entkoppeln die E / A-Pins von bestimmten Peripheriefunktionen, sodass jeder der E / A-Pins einer beliebigen Peripheriefunktion zugewiesen werden kann.
Natürlich müssen die eigentlichen IO-Pins zuerst in 'board.h' definiert werden.
#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
Nach der Konfiguration der Hardware-Ressourcenzuordnung können Entwickler schließlich über die SPI-Schnittstelle mit externen Sensorchips kommunizieren.