目录导航
目前了解到,BTStack、Bluez,在linux上Bluez直接移植即可,但是在RTOS上移植,Bluez太大了,要做的工作太多;即使移植BTStack的工作量也是有的。Btstack是开源免费的协议栈,关键点是:免费!
好久没有看蓝牙了,n年前做过蓝牙的协议,只是BLE和音频。这边先回顾一下蓝牙的一些知识,再进行移植。
蓝牙协议基础知识
从蓝牙1.0到蓝牙5.3更新的特性
本节知识转载:https://baijiahao.baidu.com/s?id=1750702861899383212&wfr=spider&for=pc,我做了一些删减,优化,内容如下(下面内容比较笼统,个人觉得只是讲了一些特性,当时还算比较详细,可以初步不同版本的蓝牙到底增加了哪些特性,想要更加详细的深入,还得看整个蓝牙框架,当然,总体来说蓝牙框架是非常复杂的):
自1999年第一个蓝牙版本诞生,蓝牙技术已经发展了20余年,从蓝牙1.0到蓝牙5.3,不断更新的蓝牙协议升级了哪些功能?
蓝牙1.0
传输速率748~810kpbs,单工传输,通信易受干扰,难以区分主副设备。
蓝牙1.1
传输速率在748~810kpbs,只能以单工的传输方式进行工作,容易受到同频率产品的通信干扰,已可进行主副设备区分。该版本支持Stereo音效的传输要求,但是频宽、频率、响应时间等参数指标达不到要求,也不算是一个应用在Stereo传输上最好的协议。
蓝牙1.2
传输速率未变,在蓝牙1.1版本的基础上 ,增加了抗干扰跳频功能,支持单通道播放,但是性能还是不理想。
蓝牙2.0
蓝牙2.0是1.2的优化提升版本,传输速率能达到2Mpbs左右。该版本蓝牙模块可以实现全双工的工作方式,可以在传输文件的同时传输语音信息,进行实时双向通信。功耗相对降低,开始支持立体声。
蓝牙2.1
该版本蓝牙模块具备了手机间的配对和近场通讯NFC(Near Field CoMMunication)机制;
Sniff Subrating功能:可以实现设定两个设备间的确认数据发送间隔,当我们延长这个时间间隔就可以让蓝牙芯片的功耗降低。
该版本的蓝牙协议支持全双工通信模式,数据可实现实时双向交互。
蓝牙3.0
使用全新的协议,传输速率能够达到24Mbps,传输速率在蓝牙2.0的基础上大大提升,支持视频传输。
蓝牙4.0
实现极致的低功耗;低成本、低时延,可实现3ms的低延迟,还有AES-128加密,在保证性能的前提下实现较高的安全性。设备可多连,理论上能够实现100米的距离传输。4.0以后的蓝牙版本属于低功耗蓝牙。
蓝牙4.1
实现通过IPV6协议连接到网络,提升用户入网便捷性和使用体验;
AES加密技术:通过硬件加密技术让我们获得更加安全的连接。
蓝牙4.2
通过6LoWPAN接入互联网,提升数据传输速率;传输速率提升,安全性加强。
蓝牙5.0
更先进的蓝牙芯片,支持左右声道独立接收音频,数据处理能力更强、延迟更低。
提升传输距离:可以达到300米
Beacon:提供广播服务
无损传输:支持24bit/192KHz的无损音源传输
蓝牙5.1
AOA功能:可以实现室内定位误差1m内,室内定位更加精准。
蓝牙5.2
传输速率42Mbps,理论传输距离300米。增加了增强型ATT协议,LE功耗控制和LE同步信道等功能。
多主多从:主从一体角色下可同时连接7个从设备,并且可以作为从角色被另一个主角色设备连接。
Long Range模式:有效提高传输距离,增加通信范围。
支持大广播包:有效提升传输速率。
扩展广播包复用信道:复用37个信道传输,抗干扰能级强,传输快。
LE功率控制功能:根据信号强度变化来动态优化连接设备之间的传输功率,同时保证信号质量与减少功率浪费。
蓝牙5.3
传输速率与蓝牙5.2相同,延迟更低、续航更长、抗干扰能力更强。
支持包含广播数据信息(ADI)的周期性广播,有效提高通信效率;
ADI包括广播数据ID(DID)及广播组ID(SID),广播数据ID主要用于区分不同的广播数据内容,广播组ID用于区分不同的广播组。
新增LE增强版连接更新功能,轻松实现低功耗;
新增LE频道分级功能,可减小设备间的相互干扰;
新增Host设定Controller密钥长度的功能,安全性提高;
彻底删除高速配置(HS)及相关技术规范。
协议栈框图
这里有个概念,蓝牙 4.0 以后的版本分为两种模式,单模蓝牙和双模蓝牙:
单模蓝牙:即低功耗蓝牙模式,是蓝牙 4.0 中的重点技术,低功耗,快连接,长距离。
双模蓝牙:支持低功耗蓝牙的同时还兼容经典蓝牙,经典蓝牙的特点是大数据高速率,例如音频、视频等数据传输。
简单的讲,比如蓝牙鼠标,蓝牙手环等,基本都属于单模蓝牙;手机内的蓝牙,都属于双模蓝牙,有些高级的蓝牙音响,也是双模蓝牙。
网上找了一下,完整的协议栈的框图:
其中Transport layer是controller的部分,这个层对应着蓝牙芯片的物理层,若通过串口,那么Transport layer就是串口,如果是USB,那么对应的就是串口。
其中,比较核心的有两层,HCI和L2CAP(Logical Link Control Adaptation Protocol,逻辑链路控制和适配协议)。HCI与协议建立了桥梁,中转给Transport layer层。HCI上层一般称为profile,简单的理解是协议中应用。
其中,A2DP是音频的协议,SPP是文件传输的协议(现在都是BLE通讯),HFP/HSP是免提电话协议。
HCI(Host Controller Interface)通讯,分为Host和Controller两部分,侠义上,协议栈是指Host,其实Controller也有一部分协议栈,包括LC、LMP等。
空中包大多数通过hci接口都是能查看到的,除了少部分LMP包看不到,但是关系不大;通过Btstack是直接可以抓取hcilog的,运行BTStack程序时,会生成hci_dump.pklg文件,可以使用WireShark打开此文件。
HCI协议栈应用开发中经常看到 HCI 的身影,它对上层 Host 提供 Controller 的功能接口,将Link Layer 提供的功能封装成 Command/Event 组,所以称作 Host Controller Interface。
这些 Command/Event 包括广播、扫描、连接建立的相关操作,这些都可以通过 hcitool 命令进行测试
Command 格式:
OCF(Opcode Command Field)表示特定的 HCI 命令
OGF(Opcode Group Field)表示该 HCI 命令所属组别
它们共同组成 16 位操作码;Parameter Total Length 表示所有参数总长度
所有 BLE 相关的 HCI Command 的 OGF 都是 0x08
Event 格式:
Host端:
Logical Link Control Adaptation Protocol(L2CAP,逻辑链路控制和适配协议)。 L2CAP 对 LL 进行了一次简单封装,LL 只关心传输的数据本身,L2CAP 就要区分是加密通道还是普通通道,同时还要对连接间隔进行管理。它实现逻辑信道的多路复用(multiplexing),对上层数据进行分割和重组,以及后续的流控、错误控制和重传等。
Attribute Protocol(ATT,属性协议)。 ATT 层用来定义用户命令及命令操作的数据,比如读取某个数据或者写某个数据。BLE 协议栈中,开发者接触最多的就是 ATT。BLE 引入了 attribute 的概念,用来描述一条一条的数据。Attribute 除了定义数据,同时定义该数据可以使用的 ATT 命令,因此这一层被称为 ATT 层。
a.为每个 Attribute 定义了三个属性:
- Type,即 Attribute 的类型,使用 UUID 区分
- Handle,服务端用来唯一标识 Attribute 的 16-bit数值
- Value,Attribute 的值 为每个 Attribute
b.定义了一系列权限,方便服务端控制客户端的行为,包括访问/加密/认证/授权
c.对于不同的Attribute,客户端对服务端的访问方式也不一样,包括 Find/Read/Write
d.传输过程是在 L2CAP 的基础上,使用基于通道的多路复用,CID 为 0x0004
Security Manager(SM,安全管理协议)。 SMP 用来管理 BLE 连接的加密和安全的,如何保证连接的安全性,同时不影响用户的体验,这些都是 SMP 要考虑的工作。
Generic Access Profile(GAP,通用访问配置文件)。 GAP 是对 LL 层 payload(有效数据包)如何进行解析的两种方式中的一种。GAP 简单的对 LL payload 进行一些规范和定义,因此 GAP 能实现的功能极其有限。GAP 目前主要用来进行广播,扫描和发起连接等。GAP 定义了蓝牙设备的通用的访问功能,与 GATT 的数据通信过程对应,处理无连接及连接建立过程的通信,也就是为广播、扫描、发起连接这些过程定义统一规范。
定义了用户接口的基本参数,包括蓝牙地址、名称、pincode、class 等概念
定义了设备的角色:
-Broadcaster Role:正在发送 advertising events 的设备
-Observer Role:正在接收 advertising events 的设备
-Peripheral Role:接受 Link Layer 连接的设备(对应 Link Layer 的 slave 角色)
-Central Role:发起 Link Layer 连接的设备(对应 Link Layer 的 master 角色)
定义了通信的过程和操作模式:
-Broadcast mode and observation procedure:实现单向的、无连接的通信
-Discovery modes and procedures:实现蓝牙设备的发现操作
-Connection modes and procedures:实现蓝牙设备的连接操作
-Bonding modes and procedures:实现蓝牙设备的配对操作
Generic Attribute Profile(GATT,通用属性配置文件)。 GATT 用来规范 attribute 中的数据内容,并运用 group(分组)的概念对 attribute 进行分类管理。没有 GATT,BLE 协议栈也能跑,但互联互通就会出问题,也正是因为有了 GATT 和各种各样的应用 profile,BLE 摆脱了 ZigBee 等无线协议的兼容性困境,成了出货量最大的 2.4G 无线通信产品。
Attribute 只是将信息(或者说通信数据)做一下抽象,但是真正对抽象的信息做分类管理则是 GATT 来完成
GATT 提供了这样一种通用的、信息存储与共享的 profile framework,实现 BLE 双向通信
事实上,目前几乎所有的BLE应用都基于 GATT 实现通信
GATT 的层次结构:
GAP和GATT区别:
—GAP 定义了 BLE 网络堆栈的一般拓扑。
—GATT 详细描述了一旦设备建立连接后如何传输属性(数据)。
GATT 特别关注如何根据其描述的规则格式化打包和发送数据。在 BLE 网络堆栈中,属性协议(ATT)与 GATT 紧密对齐,GATT 直接位于 ATT 的顶部。GATT 实际上使用 ATT 来描述如何从两个连接的设备交换数据。
通用访问配置文件GAP
BLE 设备可以使用两种机制与外界通信:广播或连接。这些机制受通用访问配置文件(GAP)准则的约束。GAP 定义了启用 BLE 的设备如何使其自身可用,以及两个设备如何直接相互通信。
GATT客户端与服务端的关系
一个示例如下:手环采集了心跳信息,希望计算机读取该信息。手环充当服务端并提供信息。手机充当客户端,读取该信息。
GAP 和 GATT 模型角色基本上彼此独立从机设备或主机设备都可以充当服务端或客户端,这取决于数据的流动方式。
在一般的主从机通信时,主机可以通过读写从机的属性,实现接收和发送数据给从机,从机可以通过发送通知的方式实现与主机的通信。因此,一般从机是作为 GATT 的服务端,主机作为 GATT 的客户端。
另附上一些芯片,看一下其内部协议框架:
1、TI 的 CC26 系列芯片协议栈结构图
2、Nordic 的 nRF52 系列芯片的协议栈结构图
关于HCI中的H4和H5共存问题
针对双模蓝牙,H4和H5协议都支持,那么就需要共存。
在双模蓝牙设备中,一边播放音乐(使用经典蓝牙协议,通常是 A2DP)同时进行 BLE 传输(使用 BLE 协议),H4 和 H5 协议是如何共存并不互相干扰的问题,关键在于硬件和协议栈的设计。让我们具体分析一下这个问题。
双模蓝牙设备的工作原理
蓝牙经典协议(Classic Bluetooth,H4):
蓝牙耳机通常使用 A2DP(高级音频分配协议)来传输音频数据,它依赖于蓝牙经典协议(H4)。
音频流的数据传输通过 SCO(同步连接导向)或 ACL(异步连接导向)链路进行。
蓝牙低功耗协议(BLE,H5):
BLE 用于低功耗传感器和短距离数据传输,主要用于设备之间的小数据包交换。BLE 使用 H5 协议进行控制和数据传输。
BLE 数据包通常较小,且传输周期性较长,旨在提供低功耗、高效的通信。
H4 和 H5 的共存与隔离
1. 硬件和协议栈设计:
在双模蓝牙设备中,H4 和 H5 通常并不会直接互相干扰,原因在于硬件和协议栈通常是独立的。具体来说,设备的蓝牙控制器(如一个专用的蓝牙芯片)会负责不同协议的处理:
硬件层的资源隔离: 蓝牙控制器可能有多个无线电模块(比如一个用于经典蓝牙,另一个用于 BLE)。这意味着在不同的协议栈运行时,它们在物理层面是独立的,不会造成直接干扰。
多任务操作: 现代蓝牙芯片(如 Qualcomm、Broadcom、Nordic Semiconductor 的蓝牙芯片)支持同时运行多个协议栈。设备可能在不同的时隙或不同的频段内分别处理经典蓝牙和 BLE 数据。经典蓝牙和 BLE 协议栈会被分配不同的时间片来避免相互干扰。
2. 蓝牙时间分片和频谱管理:
蓝牙技术通过 时分复用(TDM) 来管理资源。当同时运行经典蓝牙和 BLE 时,蓝牙设备会通过时分复用将无线电资源按时间切换,确保经典蓝牙和 BLE 协议在不同的时间段内使用蓝牙芯片。这种方式通过时间片来确保通信的高效性和不冲突。
经典蓝牙和 BLE 通常工作在不同的频段上(例如经典蓝牙使用 2.4 GHz ISM 带宽,而 BLE 使用低功耗的特定信道),因此它们在物理层面上相对独立,减少了频谱冲突的可能。
3. 协议栈的协作:
多协议栈的调度:大部分双模蓝牙模块会采用一种 多任务调度方式,即在同一芯片上同时处理两种协议的数据传输,但会通过调度程序来确保它们在不同时间、不同任务下各自工作。这种调度方式在硬件层面被优化,以避免冲突。
例如,在某些低功耗 BLE 数据传输期间,经典蓝牙模块会被挂起,直到 BLE 数据传输完成。反之,当经典蓝牙正在传输音频流时,BLE 协议栈也可能会被暂时挂起,直到经典蓝牙传输完成。
实际案例中的情况
例如,蓝牙耳机与传感器的组合设备,可能会同时使用 A2DP 协议播放音乐和 BLE 协议传输数据,但这两个协议的运行通常是通过硬件和协议栈的分离来实现共存。常见的做法是:
音频流优先: 因为播放音乐需要持续、稳定的带宽,因此通常经典蓝牙协议(A2DP)会被优先保证其带宽和时间资源。BLE 数据传输会在空闲时隙进行,或使用 间歇式的数据传输,保证不会打断音频流。
BLE 间歇性传输: BLE 的数据传输通常是周期性的,即数据传输发生在非常短暂的时隙(例如每隔几百毫秒进行一次),这使得它不会对持续的音频流产生显著影响。
总结:
硬件设计: 双模蓝牙模块使用独立的硬件资源处理经典蓝牙和 BLE 协议,确保两者的并行运行不冲突。
时间分片与调度: 蓝牙协议栈通过时分复用(TDM)或智能调度,确保经典蓝牙和 BLE 通信在不同的时间窗口内运行,避免资源冲突。
BLE 的低功耗特性: BLE 的数据传输非常短暂且周期性,这意味着它不会长时间占用无线电资源,因此不会干扰经典蓝牙的音频传输。
因此,H4 和 H5 协议可以在双模蓝牙设备中共存,且不互相干扰,前提是硬件和协议栈的设计能够有效管理这两个协议的资源调度。
移植准备(下载SDK)
BTStack的源码在GITHUB上: 链接
解压后,包含的目录解释:
3rd-party: 第三方库,如md5加密,编码、解码等。
chipset: 支持的蓝牙控制器芯片组,如csr、cc256x、bcm等蓝牙芯片。
doc: btstack的说明文档。
example: 各种profile和service 的例程,如spp协议例程有spp_counter、spp_flowcontrol等。
platform: 支持btstack的操作系统或MCU架构,如freertos(嵌入式实时系统),embedded(裸机系统),posix(linux系统),windows等。
port: 完整项目启动(操作系统/MCU 与 芯片组组合),一个项目的入口就在这里;如windows-h4(在windows平台跑,使用uart传输的案例),stm32-f4discovery-cc256x(在stm32f4跑,使用uart传输,蓝牙芯片是cc256x的案例)。
src: 蓝牙协议栈的核心源码,如HCI、L2CAP、RFCOMM、SDP、SPP等。
test: 单元测试和PTS测试。
tool: 辅助工具。
源码分析
见参考附录<4>
移植函数说明
移植BTStack,和移植其他库基本大同小异,主要是实现一些硬件级别的函数,如定时器,终端,收发函数等;当然,最重要的是移植完了测试问题,能完整测试通过这些功能,并且遇到遇到问题能快速解决。
1.修改需要移植的函数
uint32_t hal_time_ms(void); 实现这个函数要返回一个系统时间戳,这个系统时间一般是每毫秒增加1,不然协议栈的某些层可能会因为超时问题导致错误。协议栈需要以这个为时间基准,做一些定时任务,如超时断开、超时重传等。
void hal_cpu_disable_irqs(void); 屏蔽cpu中断。
void hal_cpu_enable_irqs(void); 启动cpu中断。
void hal_cpu_enable_irqs_and_sleep(void); 启动cpu中断并且设置cpu休眠。
void hal_stdin_setup( void ( * handler)(char c) ); 设置一个函数,函数类型是void ( * handler)(char c),这个的用法还不太明白,不设写个空函数协议栈也能运行,TODO。
static void stdin_rx_complete(void); TODO,还不太明白这个函数的作用,先写个空函数放着,协议栈也能跑。
void hal_uart_dma_set_sleep(uint8_t sleep); TODO,还不太明白这个函数的作用,先写个空函数放着,协议栈也能跑。
void hal_uart_dma_init(void); 实现这个函数要硬件复位蓝牙模组(使用单片机GPIO产生一个复位信号),不同模组的复位电平不同,这个要看模组。
void hal_uart_dma_set_block_received( void ( * the_block_handler)(void)); 用一个函数指针 rx_done_handler 指向the_block_handler函数,即rx_done_handler = the_block_handler; the_block_handler是框架里的一个数据接收完成回调函数,当接收完蓝牙模组传给单片机的数据时需要调用这个函数,协议栈才知道数据接收完成,可以开始分析数据。实际就是调用了btstack_uart_block_embedded.c文件的 btstack_uart_block_received 函数。
void hal_uart_dma_set_block_sent( void ( * the_block_handler)(void)); 用一个函数指针 tx_done_handler 指向the_block_handler函数,即tx_done_handler = the_block_handler; the_block_handler是框架里的一个数据发送完成回调函数,当单片机发送完数据到蓝牙模组时需要调用这个函数,协议栈才知道数据发送完成,然后才能启动下一轮数据的发送。实际就是调用了btstack_uart_block_embedded.c文件的 btstack_uart_block_sent 函数。
void hal_uart_dma_set_csr_irq_handler( void ( * the_irq_handler)(void)); TODO,还不太明白这个函数的作用,先写个空函数放着,协议栈也能跑。
int hal_uart_dma_set_baud(uint32_t baud); 实现串口初始化,如果在自己的代码中已经调用了串口初始化函数,那这个函数可以空着,不过尽量还是在这里调用吧,毕竟这是框架的接口,代码比较规范,可读性也强点。
void hal_uart_dma_send_block(const uint8_t * data, uint16_t size); 这里需要实现串口发送数据功能,可以用DMA发送,也可以用循环加获取标志位的方式,用DMA发送就需要在对应的DMA通道中断函数里调用发送完成回调函数(*tx_done_handler)();,如果使用循环加获取标志位的方式就在循环发送完成时调用(*tx_done_handler)();。
void hal_uart_dma_receive_block(uint8_t * data, uint16_t size); 这里需要实现串口数据接收功能,注意需要将接收到的数据放在data所指的内存中,并且在接收完成时,调用 (*rx_done_handler)(); ,这里也可以使用DMA传输或中断传输,我是先设置DMA将串口数据放在一个缓存中,在串口中断里将缓存的数据放到环形数组里,然后在主循环里吧数据读到data所指的内存中,哈哈哈,搞得有哦点复杂了,没办法之前试的方法没成功,先将就着用等后面有空研究一下用DMA的方式直接将数据存到data中,并且能稳定的产生一个DMA中断(现在遇到一个问题就是有时候不会产生DMA中断)。
void hci_log_through_frontline(uint8_t packet_type, uint8_t in, uint8_t * packet, uint16_t len); 这里实现hci_log日志,发现有些btstack版本没有调用这个接口,如果没有的话就自己在hci_dump_embedded_stdout.c 文件的hci_dump_embedded_stdout_packet函数内调用一下,一般放在第一句。
int _write(int file, char * ptr, int len); 这里需要调用串口发送函数,与上面不同的是,这里发送的是普通日志信息,不是发给蓝牙模组的数据,也不是hci_log日志,其实就是在将printf函数重定向,使用串口输出;我这里使用的数gcc编译器所以重写的是int _write(int file, char * ptr, int len)函数,如果使用keil编译的话就需要重写 int fputc(int c, FILE * f)函数。
2、需要调用的函数
( * tx_done_handler)(); 在数据发送完成调用这个函数,在 hal_uart_dma_send_block 中有提到。
( * rx_done_handler)(); 在数据接收完成时调用这个函数,在 hal_uart_dma_receive_block 中有提到。
( * stdin_handler)(stdin_buffer[0]); TODO,还不太明白这个函数的作用。
stdin_rx_complete(); TODO,还不太明白这个函数的作用。
通过以上函数,其实移植变得很简单了,只要稍微有点C语言基础,即可开始移植。
参考附录
以上部分内容参考链接,如下:
<1>蓝牙BLE协议讲解(这个文章讲的很好): 链接
<2>Android 蓝牙协议栈源码路径 蓝牙协议栈开发工程师: 链接
<3>btstack源码分析—基于单片机裸机运行的驱动层架构介绍: 链接
<4>btstack源码分析—HCI层数据处理:链接