bluetooth
TI의 BLE 스택 시작하기
수색…
BLE 슬레이브 장치에 연결
소개
TI (Texas Instruments)의 CC26XX 시리즈 SoC는 BLE (Bluetooth Low Energy) 애플리케이션을 겨냥한 무선 MCU로서 즉시 사용 가능하다. TI는 MCU와 함께 필요한 API 및 샘플 코드를 제공하는 본격적인 소프트웨어 스택 을 제공하여 개발자가 툴 체인을 신속하게 시작할 수 있도록 지원합니다. 그러나 초보자의 경우 참조 문서 및 코드의 긴 목록 앞에서 어디에서 시작해야하는지에 대한 질문이 항상 있습니다. 이 메모는 첫 번째 프로젝트를 진행하는 데 필요한 단계를 기록하는 것을 목표로합니다.
단순 주변 장치 프로파일은 MCU가 업스트림 호스트의 BLE 주변 장치 또는 PC 및 스마트 폰과 같은 BLE 서비스 클라이언트로 작동하는 BLE 스택의 'Hello World'예제입니다. 일반적인 실제 응용 분야로는 블루투스 헤드폰, 블루투스 온도 센서 등이 있습니다.
시작하기 전에 먼저 프로그래밍 및 디버깅을위한 기본 소프트웨어 및 하드웨어 도구를 수집해야합니다.
BLE 스택
공식 웹 사이트에서 TI의 BLE-STACK-2-2-0을 다운로드하여 설치하십시오. 기본 위치 'C : \ ti'에 설치되었다고 가정하십시오.
IDE - 두 가지 옵션이 있습니다.
ARM 용 IAR Embedded Workbench. 이것은 무료 평가 기간 인 30 일 상용 도구입니다.
TI의 코드 컴포저 스튜디오 (CCS). TI의 공식 IDE 및 무료 라이센스를 제공합니다. 이 예제에서는 CCS V6.1.3
하드웨어 프로그래밍 도구
TI의 XDS100 USB 인터페이스 JTAG 디바이스 권장.
CCS에서 예제 프로젝트 가져 오기
Simple Peripheral Profile 샘플 코드는 BLE-Stack 설치와 함께 제공됩니다. 이 예제 프로젝트를 CCS로 가져 오려면 아래 단계를 수행하십시오.
- CCS를 시작하고 작업 공간 폴더를 만듭니다. 그런 다음 파일 -> 가져 오기. '가져 오기 소스 선택'에서 '코드 작성 스튜디오 -> CCS 프로젝트'옵션을 선택하고 '다음'을 클릭하십시오.
- 'C : \ ti \ simplelink \ ble_sdk_2_02_00_31 \ examples \ cc2650em \ simple_peripheral \ ccs'로 이동하십시오. 두 개의 프로젝트가 발견 될 것입니다. 모두 선택하고 아래 두 가지 옵션을 모두 선택하십시오. 그런 다음 '마침'을 클릭하십시오. 프로젝트를 작업 공간으로 복사하면 이후의 모든 수정 작업에 대해 원래 프로젝트 설정이 변경되지 않습니다.
Simple Peripheral Profile 예제에는 두 개의 프로젝트가 있습니다.
- simple_peripheral_cc2650em_app
- simple_peripheral_cc2650em_stack
'cc2650em'은 TI의 cc2650 평가 보드 용 코드 명이다. _stack 프로젝트에는 TI의 BEL-Stack-2-2-0 코드 및 바이너리가 포함되어 있는데, 이는 Bluetooth 광고, 핸드 쉐이크, 주파수 동기화 등을 처리합니다. 이것은 상대적으로 안정적이어서 원하지 않는 코드의 일부입니다 대부분의 경우 개발자가 손을 댔습니다. _app 프로젝트는 개발자가 자신의 작업과 BLE 서비스를 구현하는 곳입니다.
빌드 및 다운로드
'프로젝트 -> 모두 빌드'메뉴를 클릭하여 두 프로젝트를 빌드하십시오. 컴파일러에서 링크 할 때 내부 오류를보고하면 링커에 대해 'compress_dwarf'옵션을 다음과 같이 비활성화하십시오.
- 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 'Propoerties'를 선택하십시오.
- 'Build-> ARM Linker'에서 'Edit Flags'버튼을 클릭하십시오.
- 마지막 옵션을 '--compress_dwarf = off'로 수정하십시오.
두 프로젝트가 모두 성공적으로 빌드 된 후 '실행 -> 디버그'를 클릭하여 스택 및 앱 이미지를 모두 MCU로 다운로드하십시오.
코드 터치
샘플 코드를 적극적으로 수정하려면 개발자가 BLE 스택의 계층 구조에 대한 자세한 지식을 습득해야합니다. 온도 읽기 / 알림과 같은 기본 작업의 경우 PROFILES / simple_gatt_profile.c (.h) 및 Application / simple_peripheral.c (.h) 파일에만 집중할 수 있습니다.
simple_gatt_profile.c
모든 Bluetooth 응용 프로그램은 특정 유형의 서비스를 제공하며 각 서비스는 일련의 특성으로 구성됩니다. 단순한 주변 장치 프로파일은 5 개의 특성으로 구성된 0xFFF0의 UUID를 갖는 하나의 간단한 서비스를 정의합니다. 이 서비스는 simple_gatt_profile.c에 지정됩니다. 간단한 서비스의 요약은 다음과 같습니다.
이름 | 데이터 크기 | UUID | 기술 | 재산 |
---|---|---|---|---|
simplePeripheralChar1 | 1 | 0xFFF1 | 특성 1 | 읽기 및 쓰기 |
simplePeripheralChar2 | 1 | 0xFFF2 | 특성 2 | 읽기 전용 |
simplePeripheralChar3 | 1 | 0xFFF3 | 특성 3 | 쓰기 전용 |
simplePeripheralChar4 | 1 | 0xFFF4 | 특성 4 | 알림 |
simplePeripheralChar5 | 5 | 0xFFF5 | 특성 5 | 읽기 전용 |
다섯 가지 특성은 서로 다른 속성을 가지며 다양한 사용자 사례의 예입니다. 예를 들어, MCU는 simplePeripheralChar4를 사용하여 클라이언트의 업스트림 호스트에 정보 변경에 대해 알릴 수 있습니다.
블루투스 서비스를 정의하려면 속성 테이블을 구성해야합니다.
/*********************************************************************
* 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
},
...
};
속성 테이블은 서비스의 UUID (이 경우 0xFFF0)를 지정하는 기본 'primaryServiceUUID'로 시작합니다. 그런 다음 서비스를 구성하는 모든 특성을 선언합니다. 각 특성에는 몇 가지 특성, 즉 액세스 권한, 값 및 사용자 설명 등이 있습니다.이 테이블은 나중에 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 );
서비스 등록시 개발자는 특성의 '읽기', '쓰기'및 '권한 부여'에 대해 세 가지 콜백 기능을 제공해야합니다. 샘플 코드에서 콜백 함수 목록을 찾을 수 있습니다.
/*********************************************************************
* 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
};
따라서 서비스 클라이언트가 Bluetooth 연결을 통해 읽기 요청을 보내면 simpleProfile_ReadAttrCB가 호출됩니다. 마찬가지로 쓰기 요청이있을 때 simpleProfile_WriteAttrCB가 호출됩니다. 이 두 가지 기능을 이해하는 것이 프로젝트 사용자 지정의 성공을위한 핵심 요소입니다.
아래는 읽기 콜백 함수입니다.
/*********************************************************************
* @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 );
}
원래 버전에서 코드를 약간 수정했습니다. 이 함수는 헤더 주석에 설명 된 7 개의 매개 변수를 사용합니다. 이 함수는 속성의 액세스 권한을 검사하여 시작합니다 (예 : 읽기 권한이 있는지 여부). 그런 다음 'if (offset> 0)'조건을 테스트하여 이것이 더 큰 BLOB 읽기 요청의 세그먼트 읽기인지 확인합니다. 분명히이 함수는 현재 BLOB 읽기를 지원하지 않습니다. 그런 다음 요청 된 속성의 UUID가 추출됩니다. UUID에는 16 비트와 128 비트의 두 가지 유형이 있습니다. 샘플 코드는 16 비트 UUID를 사용하여 모든 특성을 정의하지만 128 비트 UUID는 PC 및 스마트 폰과 같은 업스트림 호스트에서 보편적으로 사용됩니다. 따라서 여러 줄의 코드가 128 비트의 UUID를 16 비트 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]);
마지막으로 UUID를 얻은 후에 요청 된 속성을 확인할 수 있습니다. 그런 다음 개발자 측의 나머지 작업은 요청 된 속성 값을 대상 포인터 '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;
}
쓰기 콜백 함수는 GATT_CLIENT_CHAR_CFG_UUID의 UUID를 가진 특별한 유형의 쓰기가 있다는 점을 제외하면 비슷합니다. 이것은 업스트림 호스트가 특성 통지 또는 표시를 등록하도록 요청한 것입니다. API GATTServApp_ProcessCCCWriteReq를 호출하여 요청을 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;
MCU에있는 코드의 애플리케이션 측은 쓰기 허용 특성에 대한 변경 사항을 통보 받기를 원할 수 있습니다. 개발자는 원하는 방식으로이 알림을 구현할 수 있습니다. 샘플 코드에서는 콜백 함수가 사용됩니다.
// 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 );
}
반면에 BLE 주변 장치가 업스트림 호스트에 특성 변경을 알리려고하면 API GATTServApp_ProcessCharCfg를 호출 할 수 있습니다. 이 API는 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 );
}
따라서 간단한 주변 장치 응용 프로그램이 SIMPLEPROFILE_CHAR4의 현재 값을 피어 장치에 알리려고하면 SimpleProfile_SetParameter 함수를 호출 할 수 있습니다.
요약하면, PROFILES / simple_gatt_profile.c (.h)는 BLE 주변 장치가 클라이언트에 제공하고자하는 서비스의 내용과 서비스의 특성에 액세스하는 방법을 정의합니다.
simple_peripheral.c
TI의 BLE 스택은 라이트 멀티 스레드 OS 레이어 위에 실행됩니다. MCU에 작업 부하를 추가하려면 먼저 개발자가 작업을 만들어야합니다. simple_peripheral.c는 작업의 생성, 초기화 및 정리 작업을 포함하는 사용자 정의 작업의 기본 구조를 보여줍니다. 온도 판독 및 알림과 같은 매우 기본적인 작업부터 시작하여 아래의 몇 가지 주요 기능에 중점을 둘 것입니다.
파일의 시작 부분은 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
DEFAULT_DESIRED_MIN_CONN_INTERVAL, DEFAULT_DESIRED_MAX_CONN_INTERVAL 및 DEFAULT_DESIRED_SLAVE_LATENCY 매개 변수는 Bluetooth 연결의 연결 간격을 정의합니다.이 간격은 한 쌍의 장치가 정보를 교환하는 빈도입니다. 연결 간격이 짧을수록 반응이 빨라지지만 소비 전력도 높아집니다.
DEFAULT_DESIRED_CONN_TIMEOUT 매개 변수는 연결이 손실 된 것으로 간주되기 전에 피어 응답을 수신하는 기간을 정의합니다. DEFAULT_ENABLE_UPDATE_REQUEST 매개 변수는 슬레이브 장치가 런타임 중에 연결 간격을 변경할 수 있는지 여부를 정의합니다. 통화 중 및 유휴 상태에 대해 서로 다른 연결 매개 변수를 갖는 것이 절전 측면에서 유용합니다.
SBP_PERIODIC_EVT_PERIOD 매개 변수는 태스크가 함수 호출을 주기적으로 실행하도록하는 클럭 이벤트의 기간을 정의합니다. 온도를 읽는 코드를 추가하고 서비스 클라이언트에게 알릴 수있는 완벽한 장소입니다.
주기 클럭은 SimpleBLEPeripheral_init 함수에서 시작됩니다.
// Create one-shot clocks for internal periodic events.
Util_constructClock(&periodicClock, SimpleBLEPeripheral_clockHandler,
SBP_PERIODIC_EVT_PERIOD, 0, false, SBP_PERIODIC_EVT);
이렇게하면 SBP_PERIODIC_EVT_PERIOD 기간의 시계가 생성됩니다. 그리고 타임 아웃시 SimpleBLEPeripheral_clockHandler 함수를 매개 변수 SBP_PERIODIC_EVT와 함께 호출합니다. 시계 이벤트는 다음에 의해 트리거 될 수 있습니다.
Util_startClock(&periodicClock);
Util_startClock 키워드를 검색하면이주기 클럭이 GAPROLE_CONNECTED 이벤트 (SimpleBLEPeripheral_processStateChangeEvt 함수 내부)에서 처음 트리거된다는 것을 알 수 있습니다. 즉, 일단 태스크가 호스트와 연결되면 주기적 루틴을 시작합니다.
주기적인 시계가 타임 아웃하면 등록 된 콜백 함수가 호출됩니다.
/*********************************************************************
* @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);
}
이 함수는 이벤트 벡터에 플래그를 설정하고 OS 작업 목록에서 응용 프로그램을 활성화합니다. 권장하지 않기 때문에이 콜백 함수에서 특정 사용자 작업 부하를 수행하지 않습니다. 사용자 워크로드에는 종종 BLE 스택 API 호출이 포함됩니다. 콜백 함수 내에서 BLE 스택 API 호출을 수행하면 종종 시스템 예외가 발생합니다. 대신, 우리는 태스크의 이벤트 벡터에 플래그를 설정하고 나중에 애플리케이션 컨텍스트에서 처리 될 때까지 기다립니다. 예제 태스크의 진입 점은 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();
}
}
}
작업의 스택 및 응용 프로그램 메시지 큐를 계속 폴링하는 무한 루프입니다. 또한 다양한 플래그에 대한 이벤트 벡터를 확인합니다. 그것이주기적인 루틴이 실제로 실행되는 곳입니다. SBP_PERIODIC_EVT 발견시 태스크 함수는 먼저 플래그를 지우고 동일한 타이머를 즉시 시작하고 루틴 함수를 호출합니다. 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);
}
주기 함수 안에서는 온도를 읽고 UART 요청을 생성하는 등 매우 구체적인 작업을 실행합니다. 그런 다음 SimpleProfile_SetParameter () API를 호출하여 Bluetooth 연결을 통해 서비스 클라이언트에 정보를 전달합니다. BLE 스택은 모든 저수준 작업이 무선 연결을 유지하는 것과 Bluetooth 링크를 통해 메시지를 전송하는 것을 관리합니다. 모든 개발자는 애플리케이션 특정 데이터를 수집하고이를 서비스 테이블의 해당 특성으로 업데이트해야합니다.
마지막으로, 쓰기 요청이 쓰기 허용 특성에 대해 수행 될 때, 콜백 함수가 호출됩니다.
static void SimpleBLEPeripheral_charValueChangeCB(uint8_t paramID)
{
SimpleBLEPeripheral_enqueueMsg(SBP_CHAR_CHANGE_EVT, paramID);
}
다시 말하지만이 콜백 함수는 사용자 작업에 대한 응용 프로그램 메시지 만 대기열에 넣습니다.이 메시지는 나중에 응용 프로그램 컨텍스트에서 처리됩니다.
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;
}
}
위의 예에서 SIMPLEPROFILE_CHAR1이 작성되면 사용자 코드는 SimpleProfile_GetParameter ()를 호출하여 새 값을 먼저 가져온 다음 사용자 정의 명령에 대한 데이터를 구문 분석합니다.
요약하면 simple_peripheral.c는 사용자 정의 작업 부하에 대한 사용자 작업을 만드는 방법의 예를 보여줍니다. 응용 프로그램 작업 부하를 예약하는 기본 방법은주기적인 시계 이벤트입니다. 개발자는 BLE 스택이 블루투스 연결을 통해 서비스 테이블의 정보를 피어 장치 (또는 그 반대)와 통신하는 나머지 작업을 처리하는 동안 서비스 테이블의 특성으로 정보를 처리해야합니다.
실제 세계 센서 연결
BLE 슬레이브 디바이스가 어떤 유용한 작업을 수행하기 위해서는 무선 MCU의 GPIO가 거의 항상 관련되어 있어야합니다. 예를 들어, 외부 센서로부터 온도를 읽으려면 GPIO 핀의 ADC 기능이 필요할 수 있습니다. TI의 CC2640 MCU는 패키징 유형이 다르면 최대 31 개의 GPIO를 제공한다.
하드웨어 측면에서 보면 CC2640은 ADC, UARTS, SPI, SSI, I2C 등과 같은 다양한 주변 기능을 제공합니다. 소프트웨어 측면에서 TI의 BLE 스택은 다양한 주변 장치에 대해 균일 한 장치 독립적 드라이버 인터페이스를 제공하려고합니다. 균일 한 드라이버 인터페이스는 코드 재사용 가능성을 높일 수 있지만 다른 한편으로는 학습 곡선의 기울기를 증가시킵니다. 이 노트에서는 SPI 컨트롤러를 예로 들어 소프트웨어 드라이버를 사용자 어플리케이션에 통합하는 방법을 보여줍니다.
기본 SPI 드라이버 흐름
TI의 BLE 스택에서 주변 장치 드라이버는 세 부분으로 구성되는 경우가 많습니다. 장치 독립적 인 드라이버 API 사양. 드라이버 API의 장치 특정 구현 및 하드웨어 자원의 매핑
SPI 컨트롤러의 드라이버 구현에는 세 개의 파일이 필요합니다.
- <ti / drivers / SPI.h> - 이것은 디바이스 독립적 인 API 스펙입니다.
- <ti / drivers / spi / SPICC26XXDMA.h> - CC2640 특정 API 구현
- <ti / drivers / dma / UDMACC26XX.h> - SPI 드라이버에 필요한 uDMA 드라이버입니다.
(참고 : TI의 BLE 스택의 주변 장치 드라이버에 대한 최상의 문서는 대부분이 경우 SPICC26XXDMA.h와 같은 헤더 파일에서 찾을 수 있습니다)
SPI 컨트롤러를 사용하려면 먼저 위의 세 가지 헤더 파일을 포함하는 사용자 지정 c 파일, 즉 sbp_spi.c를 만듭니다. 자연스러운 다음 단계는 드라이버의 인스턴스를 생성하고 시작하는 것입니다. 드라이버 인스턴스는 SPI_Handle 데이터 구조로 캡슐화됩니다. 다른 데이터 구조 - SPI_Params는 비트 전송률, 전송 모드 등과 같이 SPI 컨트롤러의 주요 매개 변수를 지정하는 데 사용됩니다.
#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);
}
위의 샘플 코드는 SPI_Handle 인스턴스를 초기화하는 방법을 보여줍니다. 내부 데이터 구조를 초기화하려면 먼저 API SPI_init ()를 호출해야합니다. 함수 호출 SPI_Params_init (& spiParams)는 SPI_Params 구조의 모든 필드를 기본값으로 설정합니다. 그런 다음 개발자는 특정 경우에 맞게 주요 매개 변수를 수정할 수 있습니다. 예를 들어, 위의 코드는 SPI 컨트롤러를 비트 속도 800kbps의 마스터 모드에서 작동하도록 설정하고 비 차단 방법을 사용하여 각 트랜잭션을 처리하므로 트랜잭션이 완료되면 콜백 함수 sbp_spiCallback이 호출됩니다.
마지막으로 SPI_open ()을 호출하면 하드웨어 SPI 컨트롤러가 열리고 이후의 SPI 트랜잭션에 대한 핸들이 반환됩니다. SPI_open ()은 두 개의 인수를 취합니다. 첫 번째는 SPI 컨트롤러의 ID입니다. CC2640은 2 개의 하드웨어 SPI 컨트롤러를 칩에 내장하고 있으므로이 ID 인수는 아래 정의 된대로 0 또는 1이됩니다. 두 번째 인수는 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;
SPI_Handle이 성공적으로 열리면 개발자는 SPI 트랜잭션을 즉시 시작할 수 있습니다. 각 SPI 트랜잭션은 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;
예를 들어, SPI 버스에서 쓰기 트랜잭션을 시작하려면 개발자는 전송할 데이터가 채워진 'txBuf'를 준비하고 'count'변수를 전송할 데이터 바이트의 길이로 설정해야합니다. 마지막으로 SPI_transfer (spiHandle, spiTrans)를 호출하면 SPI 컨트롤러에 신호를 보내 트랜잭션을 시작합니다.
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);
}
SPI는 전송과 수신이 동시에 발생하는 이중 프로토콜이기 때문에 쓰기 트랜잭션이 완료되면 해당 응답 데이터가 'rxBuf'에서 이미 사용 가능합니다.
전송 모드를 콜백 모드로 설정 했으므로 트랜잭션이 완료 될 때마다 등록 된 콜백 함수가 호출됩니다. 여기서 우리는 응답 데이터를 처리하거나 다음 트랜잭션을 시작합니다. (참고 : 콜백 함수 내에서 필요한 API 호출 이상을 수행하지 마십시오.)
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 핀 구성
지금까지 SPI 드라이버를 사용하는 것이 합리적으로 간단 해 보입니다. 그러나 잠깐, 어떻게 소프트웨어 API 호출을 물리적 SPI 신호에 연결할 수 있습니까? 이는 SPICC26XXDMA_Object, SPICC26XXDMA_HWAttrsV1 및 SPI_Config의 세 가지 데이터 구조를 통해 수행됩니다. 그들은 일반적으로 '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}
};
SPI_Config 배열에는 각 하드웨어 SPI 컨트롤러에 대한 별도의 항목이 있습니다. 각 항목에는 fxnTablePtr, object 및 hwAttrs의 세 필드가 있습니다. 'fxnTablePtr'은 드라이버 API의 장치 별 구현을 가리키는 점 테이블입니다.
'객체'는 드라이버 상태, 전송 모드, 드라이버 콜백 함수와 같은 정보를 추적합니다. 이 '객체'는 드라이버가 자동으로 관리합니다.
'hwAttrs'는 실제 하드웨어 리소스 매핑 데이터, 예를 들어 SPI 신호의 IO 핀, 하드웨어 인터럽트 번호, SPI 컨트롤러의 기본 주소 등을 저장합니다. 'hwAttrs'의 대부분 필드는 미리 정의되어 있으며 수정할 수 없습니다. 반면 인터페이스의 IO 핀은 사용자 기반으로 자유롭게 할당 될 수 있습니다. 참고 : CC26XX MCU는 IO 핀을 주변 장치 기능에 할당 할 수있는 특정 주변 장치 기능과 IO 핀을 분리합니다.
물론 실제 IO 핀은 '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
결과적으로 하드웨어 자원 매핑을 구성한 후 개발자는 SPI 인터페이스를 통해 외부 센서 칩과 통신 할 수 있습니다.