BTStack在所实现的协议和服务之间采用很多状态机实现相互作用,特点:
<1>单线程.BTStack只有一个单独的循环。
<2>没有阻塞,采用event事件方式。
<3>No artficially limited buffers/pools-Incoming and out going data packets are not queued
<4>Statically bouneded memory--最大连接\通道\服务数都可以配置
上图单线程应用架构。
BTStack已经实现了HCI、L2CAP、L2CAP-LE、RFCOMM、SDP和ATT。如下图所示.BTStack为HCI,L2CAP,EFCOMM和SDP提供了各种API接口,可直接使用.
BTStack有个重要的结构,service.一个service代表表示一个处理输入连接的服务器端。目前BTStack实现了两个服务,RFCOMM和L2CAP service.LECAP服务处理连接到L2CAP通道的连接,并用协议服务复用器(PSM)注册。RFCOMM服务则处理连接到RFCOMM连接的输入,并用RFCOMM通道ID注册。对于输出则不需要特别的注册,在应用需要的时候会创建.
service 结构体,活跃的连接和远端设备可分为两种不同的方式:
<1>静态的个体的内存池,最大元素在config配置文件中定义。调用btstack_memory_init函数初始化静态内存池。
<2>使用malloc/free 函数动态分配,要在config文件中定义HAVE_MALLOC.
如果同时定义了上述两种方法,则静态分配优先,反之,两者都没定义,则会报错.
函数btstack_memory_init完成内存的设置.
BTStack运行runloop来处理输入数据、安排工作.runloop处理两个来自完全不同类型源的事件:数据源和定时器.数据源表示通讯接口比如UART和USB驱动。定时器用于实现各种蓝牙相关的超时.也可用于处理周期性的事件.
数据源和定时器分别通过结构体data_source_t和timer_source_t表示.每个结构体都包含一个链接列表节点和一个指向回调函数的指针。所有活跃的定时器和数据源都保存在链接列表中。不同的是列表中的数据源没有分类,而定时器是通过失效分类的从而高效处理.完整的run loop循环运行步骤:
<1>轮训运行所有已注册的数据源的回调函数。
<2>执行已就绪的定时器的回调函数.
<3>是否有中断处理请求,没有的话则进入sleep模式.
通过UART、USB或者定时器tick的输入数据会产生中断并叫醒处理器。为了避免在run loop刚好进入睡眠模式的时候发生数据源中断,中断驱动的数据源必须调用embeded_trigger函数.该函数设置一个内部标识在刚进入睡眠模式的临界点中进行判断.
定时器都是单发的:在定时器事件处理函数被执行之前是从定时器列表中移除的.如果定时器是周期的,就在回调函数中再重新注册同样的定时器源.注意,要使用定时器,必须在config文件中定义HAVE_TICK。在代码中,run loop通过调用run_loop_init函数实现:
run_loop_init(RUN_LOOP_EMBEDED);
初始化BTStack,要先初始化内存和run loop,然后设置HCI和其他所需要的高层协议.
HCI初始化要调整BTStack以适应所采用的平台,并需要四个参数:
<1>Bluetooth hardware control: 蓝牙硬件控制API用一个定制的初始化脚本提供HCI层、制造商特殊的波特率改变命令,以及系统电量通知。也可用于控制蓝牙模块的电源模式。此外还提供一个错误处理函数hw_error,当蓝牙模块报告硬件错误的时候,会调用该函数.bt_control_t结构体封装了常用的功能,比如,bt_control_cc256x_in-stance函数返回一个指针到适合CC256X芯片的控制结构体.
b t_ c o n t r o l _t * c o n t r o l = b t_ c o n t r o l_ c c 2 5 6 x_ i n s t a n c e ( ) ;
<2>HCI 传输层实现:嵌入式系统中,蓝牙模块可通过UART,或USB端口连接。BTStack实现了两种基于UART的协议:HCI UART 传输层(H4)和带有eHCILL H4(TI的一种轻量级低功耗的变体).这可以通过连接不同的合适的文件而实现,然后获取一个指向HCI传输层实现的指针,比如:
<3>HCI传输层配置:
因为H4传输接口所使用的UART配置不是标准的,因此BTStack在Main应用中提供该配置.比如:
h c i _u a r t_ c o n f i g_ t * c o n f i g = h c i _u a r t_ c o n f i g_ c c 2 5 6 x_ i n s t a n c e ( ) ;
<4>永久存储—指定永久数据的存放比如密码,远程设备名字等.一般需要根据平台指定代码来访问MCU的EEPROM等.比如:
remote_device_db_t * remote_db=&remote_device_dv_memory;
然后即可进行HCI初始化:
hci_init(transport,config,control,remote_db);
高层只依赖于BTStack,并通过各自的*_init函数进行初始化.这些初始化函数用下面的层注册自己.
硬件和BTStack配置完成之后就进入了run loop循环.从此开始一切都是事件驱动的.应用层调用BTStack函数,这些函数可能会轮流发送命令到蓝牙模块.所导致的事件会传回到应用层.
BTStack没有为每一个可能发生的事件都实现一个单独的回调句柄,而是为事件逻辑分组并提供通用的接口。
HCI和一般的BTStack事件传递到l2cap_register_packet_handler函数所指定的数据包句柄,或hci_register_packet_handler(如果L2CAP没有使用的话).在L2CAP中,BTStack区分输入和输出连接.比如事件和数据包传递到不同的数据包句柄.输出连接用于访问远程服务,输入连接用于提供服务.对于输入连接,使用l2cap_register_service指定的句柄处理,对于输出连接,l2cap_create_channel_internal指定的句柄可处理.对于RFCOMM连接,目前采用rfcomm_register_packet_handler指定的句柄来处理所有的连接.应用层可以注册一个单独的共享的数据包句柄处理所有的协议和服务,或者对每个协议层和服务都采用各自的数据包句柄.共享的数据包句柄常用语堆栈初始化和连接管路.独自的数据包句柄可用于每个L2CAP服务和输出连接.
RFCOMM有强制的基于积分的流控制,这就意味着确立了RFCOMM连接的两个设备采用积分来追踪还有多少RFCOMM数据包可以发送到彼此.如果一个设备没有剩余积分了,它就不能再发送RFCOMM数据包,传送必须暂停.在连接建立的阶段,提供初始的积分.BTStack在两个方向追踪积分数据.如果没有输出积分,RFCOMM发送函数返回一个错误,晚会儿再试。对于接收数据,BTStack提供通道和服务用和不用自动积分管理通过不同的函数来创建或注册它们自己.
因为BTStack中的定时器都是单发(Single shot)的,对于周期性的定时器通过在timer_handler回调函数中重新注册timer_source来实现.如下所示:
每个HCI命令都赋有一个2字节的操作码(OpCode)用于唯一识别不同的命令类型.操作码分为两部分,OGF(OpCode Group Field)和OCF(OpCode Command Field).其后跟着参数的全部长度和实际参数.BTStack提供hci_cmd_t结构体作为HCI命令数据包佛如压缩格式.命令的OpCode可通过OPCODE宏来计算.如下所示:
BTStack定义的可能的OGF:
HCI命令参数所支持的格式:
BTStack命令模块例子:其中OGF_CONTROLLER_BASEBAND是OGF,0x13是OCF,参数格式’N’表示一个空的UTF-8字符串.
函数hci_send_command用于发送基于模板的HCI命令和一列参数,在发送之前需检查输出缓冲是否为空以及蓝牙模块是否准备好接收下一条命令--因为大多数蓝牙模块只允许发送单条HCI命令。可通过调用hci_can_send_packet_packet_now(HCI_COMMAND_DATA_PACKET)来完成,如果返回true则可以发送.
输出数据包,无论数据还是命令,在BTStack中都不需要排队。独立于输出缓冲的数字,数据包的产生必须与远端接收器或/和最大连接速度相适应.因此数据包只有在可以发送的时候才会产生.BTStack返回BTSTACK_ACL_BUFFERS_FULL,如果输出缓冲是满的。如果没有输出积分,会返回RFCOMM_NO_OUTGOING_CREDITS。
蓝牙设备必须设置为 discoverable 才能被其它执行查询扫描的设备发现.要设置为discoverable,应用直接调用hci_discoverable_control 带参数 1.如果希望设备有个名字,可通过发送hci_write_local_name 命令来设置.若要节能,则可以设置设备为 undiscoverable.
要扫描远端设备,采用命令 hci_inquiry.然后,蓝牙模块会主动扫描其他设备并将报告作为HCI_EVENT_INQUIRY_RESULT,HCI_EVENT_INQUIRY_RESULT_WITH_RSSI,或HCI_EVENT_EXTENDED_INQUIRY_RESPONSE事件,每个响应包括至少蓝牙地址、设备class,寻呼扫描重复模式,时钟偏移量等.后面的事件添加关于所接收的信号的信息或者提供扩展查询结果(EIR,Extended Inquiry Result).
默认情况下,RSSI或EIR都不会报告.如果蓝牙设备的蓝牙协议版本在2.1之后,那么hci_write_inquiry_mode命令可以打开这些高级功能(0,标准结果;1,RSSI;2,RSSI和EIR).
默认的蓝牙通讯是不需要授权的,任何蓝牙设备都可以和其它蓝牙设备对话.蓝牙设备可以选择需要授权以提供特殊的服务。蓝牙授权一般通过PIN码完成.PIN码是一个ASCII字符串,最多16个字符。用户需要在两个设备上输入同样的PIN码,这个过程就是配对.一旦用户输入了PIN码,两个设备会产生一个链接key.链接key可以存在蓝牙模块上,也可以放在永久存储中.下次两个设备会使用先前产生的链接key。
L2CAP是基于通道的概念.通道是基带上面的逻辑连接,每个通道会绑定到一个单独的协议(多对一的方式,即同一协议可以和很多通道绑定,但是一个通道只能绑定一种协议,多个通道可以共享同样的基带连接).
要与远端设备的L2CAP通讯,本地的蓝牙设备应用要初始化L2CAP层,使用l2cap_init()函数。然后用函数l2cap_create_channel_internal()创建到远程设备的PSM的输出L2CAP通道.如果基带连接不存在,该函数会初始化一个新的基带连接.作为L2CAP创建通道函数的输入参数的数据包句柄会分配给新的输出L2CAP通道.该句柄会接收L2CAP_EVENT_CHANNEL_OPENED和L2CAP_EVENT_CHANNEL_CLOSED事件以及L2CAP数据包.
要提供L2CAP服务,本地的蓝牙设备应用必须初始化L2CAP并用l2cap_register_service_internal函数注册该服务.然后就可以等待输入L2CAP的连接.应用也可以根据具体情况使用l2cap_accept_connection_internal 来接受输入连接或l2cap_deny_connection_internal来拒绝连接.如果连接被接受且输入L2CAP通道成功地打开,那么L2CAP服务就可以用l2cap_send_internal函数发送L2CAP数据包到所连接的设备.
此外,L2CAP数据包的发送可能会失败,这时候应用层要根据DAEMON_EVENT_HCI_PACKET_SENT(BTStack输出缓冲是空闲的)或L2CAP_EVENT_CREDITS(ACL 缓冲空闲)事件重新发送.
为了同远端设备的RFCOMM服务通信,本地蓝牙设备的应用层使用rfcomm_init函数初始化RFCOMM层,然后使用rfcomm_create_channel_internal()函数创建一个RFCOMM输出通道到给定的服务器通道.函数rfcomm_create_channel_internal-al为RFCOMM多路器初始化一个新的L2CAP通道(如果不存在的话).该通道自动为另一端提供充足的积分.如果手动提供积分,要通过调用rfcomm_create_channel_with_initial_credits_internal函数创建RFCOMM连接.数据包句柄作为RFCOMM创建通道函数的输入参数被分配给新的输出RFCOMM通道,该句柄会接收RFCOMM_EVENT_OPEN_CHAN-NEL_COMPLETE和RFCOMM_EVENT_CHANNEL_CLOSED事件和RFCOMM数据包.
发送RFCOMM数据包可能会失败,这时候,应用层可依赖DAEMON_EVENT_HCI_PACKET_SENT或RFCOMM_EVENT_CREDITS事件来重新发送.
RFCOMM有个强制的基于积分的流控制可用于调整数据速率.(手动和自动两种方式)
BTStack含有一个完整的SDP服务器,允许注册SDP记录.SDP记录是一列SDP属性{ID,Value}存储在Data Element Sequence(DES)中,其中,ID是一个16位的数字,value可以是整形数字或者字符串甚至可以包含其他的DES。
要为SPP服务创建一个SDP记录,可以调用sdp_create_spp_service(src/sdp_util.c)用一个指针指向缓冲区以存储记录、RFCOMM服务器通道数字和记录名字.对于其他类型的记录,可以使用srx/sdp_util.c中的数据单元de_*。
First, a DES is created and then the Service Record Handle and Service Class ID List attributes are added to it. The Service Record Handle attribute is added by calling the de add number
function twice: the rst time to add 0x0000 as attribute ID, and the second time to add the actual record handle (here 0x1000) as attribute value. The Service Class ID List attribute has ID 0x0001, and it requires a list of UUIDs as attribute value. To create the list, de push sequence is called, which "opens" a sub-DES.The returned pointer is used to add elements to this sub-DES. After adding all UUIDs, the sub-DES is "closed" with de pop sequence.
BTStack 提供SDP客户端可以查询远端SDP服务.sdp_client_query函数启动一个L2CAP连接到远端SDP服务器,一旦建立连接,Service Search Attribute请求用Service Search Pattern和Attribute ID List 就会发送出去.该查询的结果包括一列Service Records,每一个都包含有所请求的属性.这些记录通过SDP parser处理.parser会传递SDP_PARSER_ATTRIBUTE_VALUE和SDP_PARSER_COMPLETE事件,SDP_PARSER_ATTRIBUTE_VALUE事件会一个字节一个字节的传递属性值。
此外,也可以实现特殊的SDP查询.
For example, BTstack provides a query for RFCOMM service name and channel number. This information is needed, e.g., if you want to connect to a remote SPP service.The query delivers all matching RFCOMM services, including its name and the channel number, as well as a query complete event via a registered callback,