CC2640学习笔记——自定义广播
CC2640学习笔记——自定义广播
上文将SDK广播例程导入到了工作区,并修改工程配置使其正常编译下载到CC2640R2F芯片中。接下来笔者通过修改例程实现广播自定义数据。
一、广播信息定义
广播名称 | ATCBroadcastTest |
---|---|
蓝牙类型 | LE only(低功耗蓝牙BLE) |
广播类型 | Scannable Undirected event |
广播数据 | ATCTest |
二、GAP概述
Generic Access Profile (GAP):低功耗蓝牙协议栈的 GAP 层负责连接功能。该层处理设备的访问模式和过程,包括设备发现、链路建立、链路终止、安全功能的启动和设备配置。
- GAP状态
基于设备配置的角色,上图 GAP 状态图显示了设备的各种状态。下面描述这些状态。
- 待机:复位后设备处于初始空闲状态。
- 广播:设备使用特定数据进行广告,让任何发起设备知道它是可连接设备(此广告包含设备地址,并且可以包含一些附加数据,例如设备名称)。
- 扫描:扫描设备收到广告后,向广告商发送扫描请求。广告商以扫描响应进行响应。此过程称为设备发现。扫描设备知道广告设备并且可以发起与其的连接。
- 发起:发起时,发起者必须指定要连接的对端设备地址。如果接收到与对等设备的该地址匹配的通告,则发起设备会发出请求,以使用 GAP 连接状态中描述的连接参数与通告设备建立连接(链接)。
- Peripheral/Central:当连接形成时,如果设备是广告者,则充当 Peripheral;如果是发起者,则充当 Central。
蓝牙设备根据应用用例以一种或多种 GAP 角色运行。 GAP 角色利用一种或多种 GAP 状态。基于此配置,许多蓝牙低功耗协议栈事件直接由主应用程序任务处理,有时从 GAP Bond Manager 路由。应用程序可以向堆栈注册以获得某些事件的通知。
- GAP角色
根据设备的配置,GAP 层始终以规范定义的四 个角色之一进行操作:
- 广播者- 设备是不可连接的广告者。
- 观察者 - 设备扫描广告但无法发起连接。
- 外设 - 该设备是一个广告商,可连接并在单个链路层连接中作为外设运行。
- 中央设备 - 设备扫描广告并启动连接,并在单个或多个链路层连接中作为中央设备运行。
三、广播相关协议概述
1、广播事件
BLE广播事件有七种类型(见《Bluetooth Core Specification》V5.2 Vol6-PartB-4.4.2)。
关于各种类型的广播事件详情读者可自行研究《蓝牙核心规范》,笔者简述下接下来要使用的不定向可扫描的广播事件(Scannable Undirected event)。
2、不定向可扫描广播事件(见《Bluetooth Core Specification》V5.2 Vol6-PartB-4.4.2.5)
可扫描无向广告事件使用 ADV_SCAN_ID 作为指示标志 (ADV_EXT_IND PDU 暂不讨论)。读者有兴趣可以在实验过程中使用抓包工具查看PDU格式及内容,是否跟协议规范一致。
可扫描无向事件类型允许扫描者使用扫描请求(SCAN_REQ PDU)来响应,以请求广播者返回扫描附加信息。
如果广播者从广播过滤策略允许的扫描者接收到包含其设备地址的SCAN_REQ PDU,则其应在同一广播频道索引上使用SCAN_RSP PDU进行回复。发送SCAN_RSP PDU后,或者如果广播过滤策略不允许处理SCAN_REQ PDU,则广播者应移动到下一个使用的主要广播频道索引以发送另一个ADV_SCAN_IND PDU,或者关闭广播事件。
广播事件中两个连续ADV_SCAN_IND PDU开始之间的时间应小于或等于10ms。广播事件应在广告间隔内关闭。
未接收到SCAN_REQ PDU的广播事件的结构如下图所示。
接收到SCAN_REQ PDU并返回SCAN_RSP PDU的广播事件的结构如下图所示。
对于不定向可扫描广播事件不会相应来自广播通道上的CONNECT_IND PDU。
广播时间间隔在编写程序时讨论。
3、消息序列图
(1)低功耗蓝牙协议栈
低功耗蓝牙协议栈模型如图所示。
关于蓝牙的OSI模型和Host/Controller模型笔者理解有限,不在此献丑,有兴趣可自行研究相关文档。
实现蓝牙的不定向可扫描广播只需要跟 GAP这个对象打交道就行了。GAP上面已经进行了简要叙述。
要理解应用蓝牙广播的配置,需要了解通过应用层代码实现对协议栈 HCI和LL 的控制。
HCI描述参考TI官方协议栈对HCI的介绍。
LL描述参考TI官方协议栈对LL的介绍
(2)不定向广播消息序列
下图描述了设备B向设备A发送广播,设备B的Host层和LL层之间的信息交互过程。
四、代码实现
接下来基于上一篇 广播例程工程 实现不定向可扫描广播,周期广播自定义数据。
1、修改工程名称
将 ble5_simple_broadcaster_cc2640r2lp_app 重命名为 AtcBroadcaster_app。
将 ble5_simple_broadcaster_cc2640r2lp_stack_library 重命名为为 AtcBroadcaster_stack_library。
修改 AtcBroadcaster_app项目配置,选定 AtcBroadcaster_app 打开路径“Project->Properties->Build->Dependencies”,将ble5_simple_broadcaster_cc2640r2lp_stack_library 条目删除,添加AtcBroadcaster_stack_library。点击“Apply and Close”按钮生效更改。
重新编译两个文件夹,AtcBroadcaster_app 编译完会提示警告“Referenced project ‘ble5_simple_broadcaster_cc2640r2lp_stack_library’ does not exist in the workspace. ”,在文件“AtcBroadcaster_app/.project”中找到该警告位置删除该条目。
重新编译不再发出警告。
Ps:这个问题是笔者强迫症使然,不修改工程名称,或者忽略掉最后这个警告都不影响后续操作。
2、main 函数
打开路径“AtcBroadcaster_app ->Startup->main.c”,可以看到例程main函数如下所示
/******************************************************************************** @fn Main** @brief Application Main** input parameters** @param None.** output parameters** @param None.** @return None.*/
int main()
{/* Register Application callback to trap asserts raised in the Stack */RegisterAssertCback(AssertHandler);Board_initGeneral();#ifndef POWER_SAVING/* Set constraints for Standby, powerdown and idle mode */Power_setConstraint(PowerCC26XX_SB_DISALLOW);Power_setConstraint(PowerCC26XX_IDLE_PD_DISALLOW);
#endif // POWER_SAVING#ifdef ICALL_JTuser0Cfg.appServiceInfo->timerTickPeriod = Clock_tickPeriod;user0Cfg.appServiceInfo->timerMaxMillisecond = ICall_getMaxMSecs();
#endif /* ICALL_JT *//* Initialize ICall module */ICall_init();/* Start tasks of external images - Priority 5 */ICall_createRemoteTasks();/* Kick off application - Priority 1 */SimpleBroadcaster_createTask();BIOS_start(); /* enable interrupts and start SYS/BIOS */return 0;
}
现在简要分析一下主函数。
前两句分别是注册断言回调函数和开发板必要的初始化,可以不必理会。
接下来两个条件条件编译嵌套,查看"AtcBroadcaster_app ->TOOLS->defines->ble5_simple_broadcaster_cc2640r2lp_app_FlashROM_StackLibrary.opt”文件可以看到,声明了"POWER_SAVING"和“ICALL_JT”。
因此前一个嵌套可以忽略掉,后一个前套内设置了用户参数 user0Cfg,这是初始化蓝牙协议栈的必要参数。
ICall_init(),初始化了ICall模块, ICall 允许应用程序和协议栈在统一的 TI-RTOS 环境中高效运行、通信和共享资源。
ICall_createRemoteTasks(), 创建蓝牙协议栈任务但并未启动, 该任务优先级为 5,即任务最高优先级。前面的用户参数 user0Cfg,在这个函数里作为配置参数。
SimpleBroadcaster_createTask(), 创建用户自定义蓝牙任务,蓝牙行为和事件都在这个任务里。
BIOS_start(),开始 TI-RTOS 任务调度。
例程主函数逻辑清晰,叙述简要。在此框架下适当修改即可。
3、自定义蓝牙任务
(1) 修改文件名称
项目浏览器中打开路径 “AtcBroadcaster_app->Application”,将 “simple_broadcaster.c” 和 “simple_broadcaster.h” 两个文件名称改为 “AtcBroadcaster.c”、“AtcBroadcaster.h”。
更改 “AtcBroadcaster.h” 条件编译语句。
······
#ifndef ATCBROADCASTER_H/* SIMPLEBROADCASTER_H */
#define ATCBROADCASTER_H/* SIMPLEBROADCASTER_H */#ifdef __cplusplus
extern "C"
{
#endif
······
(2) 修改 AtcBroadcaster.c
首先使用替换工具将“Simple”字符串替换为“Atc”。
(3) 广播任务
例程广播任务代码如下所示
/********************************************************************** @fn AtcBroadcaster_processEvent** @brief Application task entry point for the Atc Broadcaster.** @param none** @return none*/
static void AtcBroadcaster_taskFxn(UArg a0, UArg a1)
{// Initialize applicationAtcBroadcaster_init();// Application main loopfor (;;){// Get the ticks since startupuint32_t tickStart = Clock_getTicks();uint32_t events;events = Event_pend(syncEvent, Event_Id_NONE, SB_ALL_EVENTS,ICALL_TIMEOUT_FOREVER);if (events){ICall_EntityID dest;ICall_ServiceEnum src;ICall_HciExtEvt *pMsg = NULL;if (ICall_fetchServiceMsg(&src, &dest,(void **)&pMsg) == ICALL_ERRNO_SUCCESS){if ((src == ICALL_SERVICE_CLASS_BLE) && (dest == selfEntity)){// Process inter-task messageAtcBroadcaster_processStackMsg((ICall_Hdr *)pMsg);}if (pMsg){ICall_freeMsg(pMsg);}}// If RTOS queue is not empty, process app message.if (events & SB_QUEUE_EVT){while (!Queue_empty(appMsgQueueHandle)){sbEvt_t *pMsg = (sbEvt_t *)Util_dequeueMsg(appMsgQueueHandle);if (pMsg){// Process message.AtcBroadcaster_processAppMsg(pMsg);// Free the space from the message.ICall_free(pMsg);}}}}}
}
AtcBroadcaster_init(), 调用这个函数,对使用蓝牙应用进行必要的配置。
在主循环中调用 Event_pend() 函数等待 ICall 事件和用户自定义事件发生,并在后面的代码处理他们。
AtcBroadcaster_processStackMsg(), 处理协议栈对应用任务发送的事件标志。
AtcBroadcaster_processAppMsg(), 处理用户自定义的时间标志。
状态机是个很好的工具,能帮助编程人员理清编程对象的行为特征,初学者建议可以多了解一下。
(4) 广播任务初始化
初始化代码如下,
/********************************************************************** @fn AtcBroadcaster_init** @brief Initialization function for the Atc Broadcaster App* Task. This is called during initialization and should contain* any application specific initialization (ie. hardware* initialization/setup, table initialization, power up* notification ...).** @param none** @return none*/
static void AtcBroadcaster_init(void)
{// ******************************************************************// N0 STACK API CALLS CAN OCCUR BEFORE THIS CALL TO ICall_registerApp// ******************************************************************// Register the current thread as an ICall dispatcher application// so that the application can send and receive messages.ICall_registerApp(&selfEntity, &syncEvent);#ifdef USE_RCOSCRCOSC_enableCalibration();
#endif // USE_RCOSC// Create an RTOS queue for message from profile to be sent to app.appMsgQueueHandle = Util_constructQueue(&appMsgQueue);// Open LCDdispHandle = Display_open(Display_Type_ANY, NULL);// Register with GAP for HCI/Host messages. This is needed to receive HCI// events. For more information, see the HCI section in the User's Guide:// (selfEntity);//Initialize GAP layer for Peripheral role and register to receive GAP eventsGAP_DeviceInit(GAP_PROFILE_BROADCASTER, selfEntity, addrMode, NULL);Display_printf(dispHandle, 0, 0, "BLE Broadcaster");
}
ICall_registerApp(), 这是要使用协议栈必须调用的函数,它注册当前任务为ICall 调度应用程序,使其能和协议栈收发数据通信。
Util_constructQueue(), 创建一个应用消息队列,用于接收协议栈发给应用任务的消息。
GAP_RegisterForMsgs(), HCI 传输协议栈和蓝牙控制单元间的命令和消息,这个函数设置用户任务可接收 HCI 消息。
GAP_DeviceInit(), GAP 设备初始化,将当前设备 GAP 角色设置为广告者。
(5) 协议栈事件处理
协议栈事件处理代码如下
/********************************************************************** @fn AtcBroadcaster_processStackMsg** @brief Process an incoming stack message.** @param pMsg - message to process** @return none*/
static void AtcBroadcaster_processStackMsg(ICall_Hdr *pMsg)
{switch (pMsg->event){case GAP_MSG_EVENT:AtcBroadcaster_processGapMessage((gapEventHdr_t*) pMsg);break;default:// do nothingbreak;}
}
可以看到例程中仅实现了一个 GAP_MSG_EVENT 事件,这事件处理在协议栈 GAP 这个对象的相关事务,蓝牙广播就是 GAP 的业务范围。因此需要着重学习 GAP 消息处理函数,在例程的基础上稍加修改就能达到效果了。
协议栈事件有很多,GAP_MSG_EVENT 只是其中很重要中的一个。协议栈定义的事件标志可以找到定义如下。
/********************************************************************** BLE OSAL GAP GLOBAL Events*/
#define GAP_EVENT_SIGN_COUNTER_CHANGED 0x4000 //!< The device level sign counter changed// GAP - Messages IDs (0xD0 - 0xDF)
#define GAP_MSG_EVENT 0xD0 //!< Incoming GAP message// SM - Messages IDs (0xC1 - 0xCF)
#define SM_NEW_RAND_KEY_EVENT 0xC1 //!< New Rand Key Event message
#define SM_MSG_EVENT 0xC2 //!< Incoming SM message// GATT - Messages IDs (0xB0 - 0xBF)
#define GATT_MSG_EVENT 0xB0 //!< Incoming GATT message
#define GATT_SERV_MSG_EVENT 0xB1 //!< Incoming GATT Serv App message// L2CAP - Messages IDs (0xA0 - 0xAF)
#define L2CAP_DATA_EVENT 0xA0 //!< Incoming data on a channel
#define L2CAP_SIGNAL_EVENT 0xA2 //!< Incoming Signaling message// HCI - Messages IDs (0x90 - 0x9F)
#define HCI_DATA_EVENT 0x90 //!< HCI Data Event message
#define HCI_GAP_EVENT_EVENT 0x91 //!< GAP Event message
#define HCI_SMP_EVENT_EVENT 0x92 //!< SMP Event message
#define HCI_EXT_CMD_EVENT 0x93 //!< HCI Extended Command Event message
#define HCI_SMP_META_EVENT_EVENT 0x94 //!< SMP Meta Event message
#define HCI_GAP_META_EVENT_EVENT 0x95 //!< GAP Meta Event message// ICall and Dispatch - Messages IDs (0x80 - 0x8F)
#define ICALL_EVENT_EVENT 0x80 //!< ICall Event message
#define ICALL_CMD_EVENT 0x81 //!< ICall Command Event message
#define DISPATCH_CMD_EVENT 0x82 //!< Dispatch Command Event message
(6) GAP 消息处理
GAP 消息处理函数如下所示,可以看出也仅实现了GAP_DEVICE_INIT_DONE_EVENT 这个事件。这个事件是在函数 AtcBroadcaster_init() 中调用了 GAP_DeviceInit() 函数初始化当前设备为 广播者后,协议栈向应用任务出的事件标志,表示设备初始化完成。
/********************************************************************** @fn AtcBroadcaster_processGapMessage** @brief Process an incoming GAP event.** @param pMsg - message to process*/
static void AtcBroadcaster_processGapMessage(gapEventHdr_t *pMsg)
{switch(pMsg->opcode){case GAP_DEVICE_INIT_DONE_EVENT:{bStatus_t status = FAILURE;gapDeviceInitDoneEvent_t *pPkt = (gapDeviceInitDoneEvent_t *)pMsg;if(pPkt->hdr.status == SUCCESS){// Store the system IDuint8_t systemId[DEVINFO_SYSTEM_ID_LEN];// use 6 bytes of device address for 8 bytes of system ID valuesystemId[0] = pPkt->devAddr[0];systemId[1] = pPkt->devAddr[1];systemId[2] = pPkt->devAddr[2];// set middle bytes to zerosystemId[4] = 0x00;systemId[3] = 0x00;// shift three bytes upsystemId[7] = pPkt->devAddr[5];systemId[6] = pPkt->devAddr[4];systemId[5] = pPkt->devAddr[3];// Set Device Info Service ParameterDevInfo_SetParameter(DEVINFO_SYSTEM_ID, DEVINFO_SYSTEM_ID_LEN, systemId);// Display device addressDisplay_printf(dispHandle, 1, 0, "%s Addr: %s",(addrMode <= ADDRMODE_RANDOM) ? "Dev" : "ID",Util_convertBdAddr2Str(pPkt->devAddr));Display_printf(dispHandle, 2, 0, "Initialized");// Setup and start Advertising// For more information, see the GAP section in the User's Guide:// /// Temporary memory for advertising parameters. These will be copied// by the GapAdv moduleGapAdv_params_t advParamLegacy = GAPADV_PARAMS_LEGACY_SCANN_CONN;#ifndef BEACON_FEATUREadvParamLegacy.eventProps = GAP_ADV_PROP_SCANNABLE | GAP_ADV_PROP_LEGACY;#elseadvParamLegacy.eventProps = GAP_ADV_PROP_LEGACY;#endif // !BEACON_FEATURE// Create Advertisement setstatus = GapAdv_create(&AtcBroadcaster_advCallback, &advParamLegacy,&advHandleLegacy);AtcBROADCASTER_ASSERT(status == SUCCESS);// Load advertising datastatus = GapAdv_loadByHandle(advHandleLegacy, GAP_ADV_DATA_TYPE_ADV,sizeof(advertData), advertData);AtcBROADCASTER_ASSERT(status == SUCCESS);// Load scan response datastatus = GapAdv_loadByHandle(advHandleLegacy, GAP_ADV_DATA_TYPE_SCAN_RSP,sizeof(scanRspData), scanRspData);AtcBROADCASTER_ASSERT(status == SUCCESS);// Set event maskstatus = GapAdv_setEventMask(advHandleLegacy,GAP_ADV_EVT_MASK_START_AFTER_ENABLE |GAP_ADV_EVT_MASK_END_AFTER_DISABLE |GAP_ADV_EVT_MASK_SET_TERMINATED);AtcBROADCASTER_ASSERT(status == SUCCESS);// Enable legacy advertisingstatus = GapAdv_enable(advHandleLegacy, GAP_ADV_ENABLE_OPTIONS_USE_MAX , 0);AtcBROADCASTER_ASSERT(status == SUCCESS);}break;}default:Display_clearLine(dispHandle, 2);break;}
}
现在修改这个函数完成自定义数据的广播。
4、自定义信息广播实现
(1) 广播 PDU 类型
advParamLegacy.eventProps = GAP_ADV_PROP_SCANNABLE | GAP_ADV_PROP_LEGACY;
当前设置为不定向可扫描的蓝牙广告。
(2) 广播间隔
例程默认参数,
/// Default parameters for legacy, scannable, connectable advertising
#define GAPADV_PARAMS_LEGACY_SCANN_CONN { \.eventProps = GAP_ADV_PROP_CONNECTABLE | GAP_ADV_PROP_SCANNABLE | \GAP_ADV_PROP_LEGACY, \.primIntMin = 160, \.primIntMax = 160, \.primChanMap = GAP_ADV_CHAN_ALL, \.peerAddrType = PEER_ADDRTYPE_PUBLIC_OR_PUBLIC_ID, \.peerAddr = { 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa }, \.filterPolicy = GAP_ADV_WL_POLICY_ANY_REQ, \.txPower = GAP_ADV_TX_POWER_NO_PREFERENCE, \.primPhy = GAP_ADV_PRIM_PHY_1_MBPS, \.secPhy = GAP_ADV_SEC_PHY_1_MBPS, \.sid = 0 \
}
其中 primIntMin , primIntMax , 表示广播间隔 数值 1 代表 0.625ms,160 * 0.625ms = 100ms。这个参数保持默认不做修改。
参数其中还有, 广播通道,过滤,Phy 等相关设置,这里不做讨论。
(3) 修改广播内容
例程广播内容:
// GAP - Advertisement data (max size = 31 bytes, though this is
// best kept short to conserve power while advertisting)
static uint8 advertData[] =
{// Flags; this sets the device to use limited discoverable// mode (advertises for 30 seconds at a time) instead of general// discoverable mode (advertises indefinitely)0x02, // length of this dataGAP_ADTYPE_FLAGS,GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED,#ifndef BEACON_FEATURE// three-byte broadcast of the data "1 2 3"0x04, // length of this data including the data type byteGAP_ADTYPE_MANUFACTURER_SPECIFIC, // manufacturer specific adv data type1,2,3#else// 25 byte beacon advertisement data// Preamble: Company ID - 0x000D for TI, refer to // Data type: Beacon (0x02)// Data length: 0x15// UUID: 00000000-0000-0000-0000-000000000000 (null beacon)// Major: 1 (0x0001)// Minor: 1 (0x0001)// Measured Power: -59 (0xc5)0x1A, // length of this data including the data type byteGAP_ADTYPE_MANUFACTURER_SPECIFIC, // manufacturer specific adv data type0x0D, // Company ID - Fixed0x00, // Company ID - Fixed0x02, // Data Type - Fixed0x15, // Data Length - Fixed0x00, // UUID - Variable based on different use cases/applications0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // UUID0x00, // Major0x01, // Major0x00, // Minor0x01, // Minor0xc5 // Power - The 2's complement of the calibrated Tx Power#endif // !BEACON_FEATURE
};
修改为
// GAP - Advertisement data (max size = 31 bytes, though this is
// best kept short to conserve power while advertisting)
static uint8 advertData[] =
{// Flags; this sets the device to use limited discoverable// mode (advertises for 30 seconds at a time) instead of general// discoverable mode (advertises indefinitely)0x02, // length of this dataGAP_ADTYPE_FLAGS,GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED,
};
广播数据最长为 31 字节,一组有效广播数据第一个字节为当前数据的长度(长度包括数据类型 1 字节 和 荷载数据 n 字节),第二个字节为数据标志, 后面的数据为荷载数据。一组有效广播数据长度为 1 + 1 + n 字节。
当前广播数据共三字节, 0x02 表示这组数据的长度,GAP_ADTYPE_FLAGS 为广播标志,GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED表示不支持 BR/EDR, 即当前设备为 LE 蓝牙设备。
(4) 修改扫描回复数据
例程扫描恢复数据为:
// GAP - SCAN RSP data (max size = 31 bytes)
static uint8 scanRspData[] =
{// complete name0x15, // length of this dataGAP_ADTYPE_LOCAL_NAME_COMPLETE,'S','i','m','p','l','e','B','L','E','B','r','o','a','d','c','a','s','t','e','r',// Tx power level0x02, // length of this dataGAP_ADTYPE_POWER_LEVEL,0 // 0dBm
};
修改为:
// GAP - SCAN RSP data (max size = 31 bytes)
static uint8 scanRspData[] =
{// complete name0x0F, // length of this dataGAP_ADTYPE_LOCAL_NAME_COMPLETE,'A', 't', 'c', 'B', 'r', 'o', 'a', 'd', 'c', 'a', 's', 't', 'e', 'r',// User define String0x08, // length of this dataGAP_ADTYPE_MANUFACTURER_SPECIFIC,'A', 'T', 'C', 'T', 'e', 's', 't'
};
和广播数据一样,扫描回复数据最大也只能到 31 字节。第一组数据定义了蓝牙全名为“AtcBroadcaster”,第二组数据定义了自定义数据为“ATCTest”。
5、结果调试
编译程序并下载到 CC2640R2F平台。
使用手机APP可以看到,收到的广播数据。
可看到全名为 “AtcBroadcaster”,在Comoany一栏数据为 “41 54 43 54 65 73 74”
对照ASCII码表可译出为"ATCTest"。
五、总结
当前项目可自行下载。
蓝牙广播者,是蓝牙调试里最简单的角色,可以很快地的调试出可观测结果。下一篇试着使用定时器计数,测量芯片温度等数据来使用更新广播数据。
发布评论