bluetooth
Rozpocznij pracę z BLE Stack TI
Szukaj…
Podłączanie do urządzeń BLE Slave
Wprowadzenie
Układy SoC z serii CC26XX firmy Texas Instruments (TI) to łatwo dostępne bezprzewodowe MCU przeznaczone do aplikacji Bluetooth Low Energy (BLE). Wraz z MCU, TI oferuje pełnowartościowy stos oprogramowania, który zapewnia niezbędne API i przykładowe kody, które pomagają szybko rozpocząć pracę z łańcuchem narzędzi. Jednak dla początkujących zawsze pojawia się pytanie, od czego zacząć przed długą listą dokumentów referencyjnych i kodów. Ta notatka ma na celu zapisanie niezbędnych kroków, które należy podjąć, aby rozpocząć pierwszy projekt.
Prosty profil urządzeń peryferyjnych to przykład stosu BLE „Hello World”, w którym MCU działa jako urządzenie peryferyjne BLE dla hostów nadrzędnych lub klientów usług BLE, takich jak komputery PC i smartfony. Typowe zastosowania w świecie rzeczywistym obejmują: słuchawki Bluetooth, czujnik temperatury Bluetooth itp.
Przed rozpoczęciem musimy najpierw zebrać podstawowe oprogramowanie i narzędzia sprzętowe w celu programowania i debugowania.
Stos BLE
Pobierz i zainstaluj BLE-STACK-2-2-0 TI z oficjalnej strony internetowej. Załóżmy, że jest zainstalowany w domyślnej lokalizacji „C: \ ti”.
IDE - są dwie opcje:
IAR Embedded Workbench for ARM. To narzędzie komercyjne z 30-dniowym bezpłatnym okresem próbnym.
Code Composer Studio (CCS) firmy TI. Oficjalne IDE TI i oferuje bezpłatną licencję. W tym przykładzie użyjemy CCS V6.1.3
Narzędzie do programowania sprzętu
Polecam urządzenie JTAG z interfejsem USB TI XDS100 .
Przykładowy projekt importu do CCS
Przykładowy kod Simple Peripheral Profile jest dostarczany z instalacją BLE-Stack. Wykonaj poniższe czynności, aby zaimportować ten przykładowy projekt do CCS.
- Uruchom CCS, utwórz folder obszaru roboczego. Następnie Plik-> Importuj. W „Wybierz źródło importu” wybierz opcję „Code Compose Studio -> Projekty CCS” i kliknij „Dalej”.
- Przejdź do „C: \ ti \ simplelink \ ble_sdk_2_02_00_31 \ Examples \ cc2650em \ simple_peripheral \ ccs”. Dwa projekty zostaną odkryte. Wybierz wszystko i zaznacz obie opcje poniżej. Następnie kliknij „Zakończ”. Kopiując projekty do obszaru roboczego, oryginalne ustawienie projektu pozostaje niezmienione dla wszystkich kolejnych modyfikacji.
Przykład prostego profilu peryferyjnego obejmuje dwa projekty:
- simple_peripheral_cc2650em_app
- simple_peripheral_cc2650em_stack
„cc2650em” to nazwa kodowa płytki ewaluacyjnej cc2650 firmy TI. Projekt _stack zawiera kody i pliki binarne BEL-Stack-2-2-0 firmy TI, który obsługuje reklamy Bluetooth, uzgadnianie, synchronizację częstotliwości itp. Jest to część kodu, która jest stosunkowo stabilna i nie chce być dotykane przez programistów przez większość czasu. W projekcie _app programiści wdrażają własne zadania i usługę BLE.
Kompiluj i pobieraj
Kliknij menu „Projekt-> Zbuduj wszystko”, aby zbudować oba projekty. Jeśli kompilator zgłasza jakiś błąd wewnętrzny podczas łączenia, spróbuj wyłączyć opcję „compress_dwarf” dla linkera przez:
- kliknij prawym przyciskiem myszy projekt i wybierz „Właściwości”.
- w „Build-> ARM Linker” kliknij przycisk „Edytuj flagi”.
- zmień ostatnią opcję na „--compress_dwarf = off”.
Po pomyślnym zbudowaniu obu projektów kliknij oddzielnie „Uruchom-> debuguj”, aby pobrać obrazy stosu i aplikacji do MCU.
Dotknij kodu
Aby móc dokonywać agresywnych modyfikacji przykładowego kodu, programiści muszą zdobyć szczegółową wiedzę na temat warstwowej struktury stosu BLE. W przypadku podstawowych zadań, takich jak odczyt / powiadomienie o temperaturze, możemy skupić się tylko na dwóch plikach: PROFILES / simple_gatt_profile.c (.h) i Application / simple_peripheral.c (.h)
simple_gatt_profile.c
Wszystkie aplikacje Bluetooth oferują określony rodzaj usługi, każda składa się z zestawu cech. Prosty profil peryferyjny definiuje jedną prostą usługę o UUID 0xFFF0, która składa się z 5 cech. Ta usługa jest określona w pliku simple_gatt_profile.c. Podsumowanie prostej usługi przedstawiono w następujący sposób.
Nazwa | Rozmiar danych | UUID | Opis | własność |
---|---|---|---|---|
simplePeripheralChar1 | 1 | 0xFFF1 | Charakterystyka 1 | Czytaj i pisz |
simplePeripheralChar2 | 1 | 0xFFF2 | Charakterystyka 2 | Tylko czytać |
simplePeripheralChar3 | 1 | 0xFFF3 | Charakterystyka 3 | Tylko pisać |
simplePeripheralChar4 | 1 | 0xFFF4 | Charakterystyka 4 | Notyfikować |
simplePeripheralChar5 | 5 | 0xFFF5 | Charakterystyka 5 | Tylko czytać |
Pięć cech ma różne właściwości i służą jako przykłady dla różnych przypadków użytkowników. Na przykład MCU może używać simplePeripheralChar4 do powiadamiania swoich klientów, hostów poprzedzających, o zmianie informacji.
Aby zdefiniować usługę Bluetooth, należy zbudować tabelę atrybutów.
/*********************************************************************
* 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
},
...
};
Tabela atrybutów zaczyna się od domyślnego „primaryServiceUUID”, który określa UUID usługi (0xFFF0 w tym przypadku). Następnie następuje deklaracja wszystkich cech składających się na usługę. Każda cecha ma kilka atrybutów, mianowicie uprawnienie dostępu, wartość i opis użytkownika itp. Tabela ta jest później rejestrowana w stosie 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 );
Po zarejestrowaniu usługi programiści muszą zapewnić trzy funkcje zwrotne dla „odczytu”, „zapisu” i „autoryzacji” cech. W przykładowym kodzie możemy znaleźć listę funkcji zwrotnych.
/*********************************************************************
* 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
};
Tak więc simpleProfile_ReadAttrCB zostanie wywołany, gdy klient usługi wyśle żądanie odczytu przez połączenie Bluetooth. Podobnie, simpleProfile_WriteAttrCB zostanie wywołany, gdy zostanie wykonane żądanie zapisu. Zrozumienie tych dwóch funkcji jest kluczem do sukcesu dostosowywania projektu.
Poniżej znajduje się odczytana funkcja zwrotna.
/*********************************************************************
* @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 );
}
Lekko zmodyfikowałem kod z jego oryginalnej wersji. Ta funkcja przyjmuje 7 parametrów, które wyjaśniono w komentarzach nagłówka. Funkcja rozpoczyna się od sprawdzenia uprawnień dostępu do atrybutu, np. Czy ma uprawnienia do odczytu. Następnie sprawdza, czy jest to odczyt segmentowy większego żądania odczytu obiektu blob, testując warunek „if (offset> 0)”. Oczywiście funkcja nie obsługuje na razie odczytu obiektów blob. Następnie wyodrębniany jest identyfikator UUID żądanego atrybutu. Istnieją dwa typy UUID: 16-bitowy i 128-bitowy. Podczas gdy przykładowy kod definiuje wszystkie cechy za pomocą 16-bitowych UUID, 128-bitowy UUID jest bardziej uniwersalny i częściej stosowany w hostach nadrzędnych, takich jak PC i smartfony. Dlatego kilka wierszy kodu jest używanych do konwersji 128-bitowego UUID na 16-bitowy 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]);
Wreszcie po otrzymaniu identyfikatora UUID możemy ustalić, który atrybut jest wymagany. Następnie pozostałym zadaniem po stronie programistów jest skopiowanie wartości żądanego atrybutu do wskaźnika docelowego „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;
}
Funkcja zwrotnego zapisu jest podobna, z tym wyjątkiem, że istnieje specjalny typ zapisu z UUID GATT_CLIENT_CHAR_CFG_UUID. Jest to żądanie hosta poprzedzającego rejestrację w celu powiadomienia lub wskazania właściwości. Wystarczy wywołać API GATTServApp_ProcessCCCWriteReq, aby przekazać żądanie do stosu 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;
Strona aplikacji kodu na MCU może chcieć być powiadamiana o każdej zmianie właściwości dopuszczalnych do zapisu. Programiści mogą implementować to powiadomienie w dowolny sposób. W przykładowym kodzie użyto funkcji zwrotnej.
// 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 );
}
Z drugiej strony, jeśli urządzenie peryferyjne BLE chce powiadomić hosty poprzedzające o każdej zmianie jego charakterystyki, może wywołać API GATTServApp_ProcessCharCfg. Ten interfejs API jest pokazany w funkcji 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 );
}
Jeśli więc prosta aplikacja peryferyjna chce powiadomić bieżącą wartość SIMPLEPROFILE_CHAR4 do urządzeń równorzędnych, może po prostu wywołać funkcję SimpleProfile_SetParameter.
Podsumowując, PROFILES / simple_gatt_profile.c (.h) definiuje zawartość usługi, którą urządzenie peryferyjne BLE chciałoby przedstawić swoim klientom, a także sposoby dostępu do tych właściwości w usłudze.
simple_peripheral.c
Stos BLE TI działa na szczycie wielowarstwowej wielowarstwowej warstwy systemu operacyjnego. Aby dodać obciążenie do MCU, programiści muszą najpierw utworzyć zadanie. simple_peripheral.c przedstawia podstawową strukturę niestandardowego zadania, która obejmuje tworzenie, inicjowanie i porządkowanie zadania. Aby rozpocząć od bardzo podstawowych zadań, takich jak odczyt temperatury i powiadamianie, skupimy się na kilku kluczowych funkcjach poniżej.
Początek pliku określa zestaw parametrów, które mogą wpływać na zachowanie połączenia 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
Parametry DEFAULT_DESIRED_MIN_CONN_INTERVAL, DEFAULT_DESIRED_MAX_CONN_INTERVAL i DEFAULT_DESIRED_SLAVE_LATENCY wspólnie określają interwał połączeń połączenia Bluetooth, czyli częstotliwość wymiany informacji przez parę urządzeń. Niższy interwał połączenia oznacza bardziej responsywne zachowanie, ale także wyższe zużycie energii.
Parametr DEFAULT_DESIRED_CONN_TIMEOUT określa, jak długo należy odbierać odpowiedź równorzędną, zanim połączenie zostanie uznane za utracone. Parametr DEFAULT_ENABLE_UPDATE_REQUEST określa, czy urządzenie podrzędne może zmieniać interwał połączenia w czasie wykonywania. Pod względem oszczędności energii przydatne są różne parametry połączenia dla faz zajętych i bezczynnych.
Parametr SBP_PERIODIC_EVT_PERIOD definiuje okres zdarzenia zegarowego, który pozwoli cyklicznie wykonywać wywołanie funkcji. Jest to dla nas idealne miejsce na dodanie kodu do odczytu temperatury i powiadomienie klientów serwisu.
Zegar okresowy jest inicjowany w funkcji SimpleBLEPeripheral_init.
// Create one-shot clocks for internal periodic events.
Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);
Spowoduje to utworzenie zegara z okresem SBP_PERIODIC_EVT_PERIOD. Po przekroczeniu limitu czasu wywoła funkcję SimpleBLEPeripheral_clockHandler z parametrem SBP_PERIODIC_EVT. Zdarzenie zegara może być następnie wywołane przez
Util_startClock(&periodicClock);
Szukając słowa kluczowego Util_startClock, możemy stwierdzić, że ten zegar okresowy jest najpierw uruchamiany w zdarzeniu GAPROLE_CONNECTED (wewnątrz funkcji SimpleBLEPeripheral_processStateChangeEvt), co oznacza, że zadanie rozpocznie okresową procedurę po ustanowieniu połączenia z hostem.
Po przekroczeniu limitu czasu zostanie wywołana jego funkcja oddzwaniania.
/*********************************************************************
* @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);
}
Ta funkcja ustawia flagę w wektorze zdarzeń i aktywuje aplikację z listy zadań systemu operacyjnego. Zauważ, że nie wykonujemy żadnego konkretnego obciążenia użytkownika w tej funkcji wywołania zwrotnego, ponieważ NIE jest to zalecane. Obciążenie użytkownika często obejmuje wywołania interfejsów API stosu BLE. Wykonywanie wywołań API stosu BLE wewnątrz funkcji wywołania zwrotnego często powoduje wyjątki systemowe. Zamiast tego ustawiamy flagę w wektorze zdarzeń zadania i czekamy, aż zostanie przetworzona później w kontekście aplikacji. Punktem wejścia dla przykładowego zadania jest 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();
}
}
}
Jest to nieskończona pętla, która ciągle odpytuje stos zadań i kolejki komunikatów aplikacji. Sprawdza również wektor zdarzeń pod kątem różnych flag. Tam właśnie wykonywana jest procedura okresowa. Po wykryciu SBP_PERIODIC_EVT funkcja zadania najpierw usuwa flagę, natychmiast uruchamia ten sam zegar i wywołuje funkcję rutynową 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);
}
Wewnątrz funkcji okresowej wykonujemy bardzo specyficzne zadanie odczytu temperatury, generowania żądań UART itp. Następnie wywołujemy interfejs API SimpleProfile_SetParameter () w celu przekazania informacji klientom usługowym za pośrednictwem połączenia Bluetooth. Stos BLE zajmuje się wszystkimi zadaniami niskiego poziomu, od utrzymywania połączenia bezprzewodowego po przesyłanie wiadomości przez łącze Bluetooth. Programiści muszą tylko zebrać dane aplikacji i zaktualizować je do odpowiednich charakterystyk w tabeli usług.
Wreszcie, gdy zostanie wykonane żądanie zapisu dla cech dozwolonych do zapisu, zostanie wywołana funkcja zwrotna.
static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}
Ponownie, ta funkcja wywołania zwrotnego kolejkuje tylko komunikat aplikacji dla zadania użytkownika, który będzie obsługiwany później w kontekście aplikacji.
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;
}
}
W powyższym przykładzie, gdy zapisany jest SIMPLEPROFILE_CHAR1, kod użytkownika najpierw pobierze nową wartość, wywołując SimpleProfile_GetParameter (), a następnie przeanalizuje dane dla poleceń zdefiniowanych przez użytkownika.
Podsumowując, plik simple_peripheral.c pokazuje przykład tworzenia zadania użytkownika dla niestandardowych obciążeń. Podstawowym sposobem planowania obciążenia aplikacji jest okresowe zdarzenie zegara. Programiści muszą jedynie przetwarzać informacje do / z cech charakterystycznych w tabeli usług, podczas gdy stos BLE zajmuje się resztą przekazywania informacji z tabeli usług do urządzeń równorzędnych (lub odwrotnie) przez połączenie Bluetooth.
Podłączanie czujników z prawdziwego świata
Aby urządzenia BLE slave wykonały jakąkolwiek przydatną pracę, GPIO bezprzewodowego MCU są prawie zawsze zaangażowane. Na przykład, aby odczytać temperaturę z zewnętrznego czujnika, może być wymagana funkcja ADC styków GPIO. MCU T26 CC2640 oferuje maksymalnie 31 GPIO, przy różnych rodzajach opakowań.
Po stronie sprzętowej CC2640 zapewnia bogaty zestaw funkcji urządzeń peryferyjnych, takich jak ADC, UARTS, SPI, SSI, I2C itp. Po stronie oprogramowania stos BLE TI próbuje zaoferować jednolity, niezależny od urządzenia interfejs sterownika dla różnych urządzeń peryferyjnych. Jednolity interfejs sterownika może zwiększyć szansę na ponowne użycie kodu, ale z drugiej strony zwiększa nachylenie krzywej uczenia się. W tej notatce wykorzystujemy kontroler SPI jako przykład i pokazujemy, jak zintegrować sterownik oprogramowania z aplikacjami użytkownika.
Podstawowy przepływ sterowników SPI
W stosie BLE TI peryferyjny sterownik często składa się z trzech części: niezależnej od urządzenia specyfikacji interfejsów API sterownika; specyficzna dla urządzenia implementacja interfejsów API sterowników i mapowanie zasobów sprzętowych.
W przypadku kontrolera SPI jego implementacja sterownika obejmuje trzy pliki:
- <ti / drivers / SPI.h> - jest to specyfikacja API niezależna od urządzenia
- <ti / drivers / spi / SPICC26XXDMA.h> - jest to implementacja interfejsu API specyficzna dla CC2640
- <ti / drivers / dma / UDMACC26XX.h> - jest to sterownik uDMA wymagany przez sterownik SPI
(Uwaga: najlepszy dokument dla sterowników peryferyjnych stosu BLE TI można znaleźć głównie w ich plikach nagłówkowych, takich jak w tym przypadku SPICC26XXDMA.h)
Aby rozpocząć korzystanie z kontrolera SPI, najpierw stwórzmy niestandardowy plik c, mianowicie sbp_spi.c, który zawiera trzy pliki nagłówkowe powyżej. Naturalnym następnym krokiem jest utworzenie wystąpienia sterownika i zainicjowanie go. Instancja sterownika jest zamknięta w strukturze danych - SPI_Handle. Inna struktura danych - SPI_Params służy do określania kluczowych parametrów kontrolera SPI, takich jak szybkość transmisji, tryb przesyłania itp.
#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);
}
Powyższy przykładowy kod pokazuje, jak zainicjować instancję SPI_Handle. Interfejs API SPI_init () musi zostać wywołany najpierw w celu zainicjowania wewnętrznych struktur danych. Wywołanie funkcji SPI_Params_init (& spiParams) ustawia wszystkie pola struktury SPI_Params na wartości domyślne. Następnie programiści mogą modyfikować kluczowe parametry, aby dopasować je do konkretnych przypadków. Na przykład powyższy kod ustawia kontroler SPI do pracy w trybie głównym z przepływnością 800 kb / s i wykorzystuje metodę nieblokującą do przetwarzania każdej transakcji, tak że po zakończeniu transakcji zostanie wywołana funkcja zwrotna sbp_spiCallback.
Na koniec wywołanie SPI_open () otwiera sprzętowy kontroler SPI i zwraca uchwyt dla późniejszych transakcji SPI. SPI_open () przyjmuje dwa argumenty, pierwszy to identyfikator kontrolera SPI. CC2640 posiada dwa sprzętowe kontrolery SPI na chipie, dlatego argumentami ID będą 0 lub 1, jak zdefiniowano poniżej. Drugi argument to pożądane parametry kontrolera 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;
Po udanym otwarciu SPI_Handle programiści mogą natychmiast inicjować transakcje SPI. Każda transakcja SPI jest opisana przy użyciu struktury danych - 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;
Na przykład, aby rozpocząć transakcję zapisu na magistrali SPI, programiści muszą przygotować „txBuf” wypełniony danymi do przesłania i ustawić zmienną „count” na długość przesyłanych bajtów danych. Wreszcie, wywołanie SPI_transfer (spiHandle, spiTrans) sygnalizuje kontrolerowi SPI rozpoczęcie transakcji.
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);
}
Ponieważ SPI jest protokołem dupleksowym, który zarówno transmituje, jak i odbiera, dzieje się w tym samym czasie, gdy transakcja zapisu jest zakończona, odpowiednie dane odpowiedzi są już dostępne na „rxBuf”.
Ponieważ ustawiamy tryb przeniesienia na tryb oddzwaniania, za każdym razem, gdy transakcja zostanie zakończona, wywoływana zostanie funkcja zarejestrowanego oddzwaniania. To tutaj przetwarzamy dane odpowiedzi lub inicjujemy kolejną transakcję. (Uwaga: zawsze pamiętaj, aby nie wykonywać więcej niż konieczne wywołania API wewnątrz funkcji zwrotnej).
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);
}
Konfiguracja pinów we / wy
Do tej pory korzystanie ze sterownika SPI wydaje się dość proste. Ale poczekaj, jak połączyć wywołania API oprogramowania z fizycznymi sygnałami SPI? Odbywa się to za pomocą trzech struktur danych: SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 i SPI_Config. Są zwykle tworzone w innej lokalizacji, np. „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}
};
Tablica SPI_Config ma osobny wpis dla każdego sprzętowego kontrolera SPI. Każdy wpis ma trzy pola: fxnTablePtr, object i hwAttrs. „FxnTablePtr” to tabela punktów, która wskazuje na specyficzne dla urządzenia implementacje interfejsu API sterownika.
„Obiekt” śledzi takie informacje, jak stan sterownika, tryb przesyłania, funkcja oddzwaniania dla kierowcy. Ten „obiekt” jest automatycznie utrzymywany przez sterownik.
„HwAttr” przechowuje rzeczywiste dane mapowania zasobów sprzętowych, np. Piny IO dla sygnałów SPI, numer przerwania sprzętowego, adres bazowy kontrolera SPI itp. Większość pól „hwAttr” jest wstępnie zdefiniowanych i nie można ich modyfikować. Natomiast piny IO interfejsu mogą być dowolnie przypisywane na podstawie przypadków użytkowników. Uwaga: MCU CC26XX odsprzęgają piny IO od określonej funkcji peryferyjnej, którą dowolne piny IO można przypisać do dowolnej funkcji peryferyjnej.
Oczywiście rzeczywiste piny IO muszą zostać zdefiniowane najpierw w „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
W rezultacie, po skonfigurowaniu mapowania zasobów sprzętowych, programiści mogą wreszcie komunikować się z układami czujników zewnętrznych za pośrednictwem interfejsu SPI.