蓝牙开发那些事(4)——关于流控

本文深入探讨蓝牙技术中的流控机制,包括HCI、LC层及L2CAP层的流控原理,详细解释了流控位的作用及如何通过流控位调整数据传输速率,以避免缓冲区溢出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一章,留了个尾巴。

先来个数据包格式的图,上图中,packet header和payload header中间都出现了流控(FLOW)位。

我们之间讲HCI命令的时候,提到了HCI transport层的流控。

其实HCI上层的L2CAP层,也提供了流控服务的。

是不是概念很搞?

接下来我们具体讲一下流控。

其实只要有传输就会有流控,在我们蓝牙的体系里,数据的流动有两个关键位置,一是介于controller和host之间的HCI,这个通过hci transport层的流控实现,我们前面讲过hci 命令,本章将继续讲到hci data的流控。

二是空中包,这个流控有可以分为LC层的流控和l2cap层的流控,LC层的流控是主要方式,通过数据包中packet header的流控位实现,l2cap层的流控通过一种特殊的s-frame(rr、rej)来实现,不过一般l2cap层的流控是不打开的,下文将展开讲。

先说packet header中的流控,packet header都是controller中的LC层自动填充的,那么这里的流控位是从哪里来的呢?

既然是LC层的东西,答案自然是controller层。

Controller接收也有一个buffer的概念,自然不可能无限制接收空中包,当controller中的buffer塞满的时候,packet header中的flow位便会置位为stop。

看上去是controller内部的行为,host改变不了对不对?

实际上我们可以做一个简单的实验的,因为数据的流动方向是,空中—>controlleràhost,假如host一直不去controller取数据的时候,空中包自然会把controller塞满。

我们可以利用hci层的流控机制,之前我们在第一章中已经讲述了hci command的流控,这里就要涉及到hci data的流控。

我们知道hci acl data是双向的,由controller流向host的是remote端发过来的数据,由host流向controller的是本地发给peer端的数据。

当打开controller到host方向上的流控的时候,首先host向controller发送host buffer size command通知到controller,大概host可以存放多少包数据,之后,controller向host上传acl data的时候,controller内部会计数,把host的buffer数减去1,当host处理掉一包数据之后,会向controller下发host number of complete packets command,来通知到controller,host释放掉一包数据的空间了,由controller内部计数,把host的buffer数加上1。

假如我们故意不下发host number of complete packets command的时候,controller的内部计数变成0了以后,就无法向host上传acl data数据了。

再过一段时间,controller的接收buffer就会满,此时我们如果看空中包的话,controller回复给远端的数据包,就会看到packet header中的flow位变成stop了。

笔者以前工作中就遇到过这样的情况,host的buffer空间捉襟见肘,只能通过流控的方式来减缓对端发送的速度。

一般来讲,HOST的buffer相对controller来说要大一些,需要使用流控的场合较少。

我们抓包的数据中,就没有controller向host方向的流控,但是host向controller方向的流控是有的。

Host向controller方向的流控的方式有packet based和data block based两种,也大同小异,只是计量方式不同而已,前者是包的个数,后者是包的大小。

我们这次抓的数据包是选择的packet based的方式:

首先host在连接阶段会向controller发送read buffer size的命令

随后controller回复了最多支持的acl data packets的数量是8个。

然后我们看看当host向controller下发acl data的时候会发生什么:

图中标出的两条数据,第一条是向远端发送的一条acl data数据,(protocol栏下面是l2cap的都是这种数据),这个时候协议栈内部是有个计数的,controller的buffer数量从8会减为7。

然后跨越千山万水,在第二个标志处,controller向host返回了number of completed packets event,其中number of completed packets的数目是1,这表示controller处理掉了一包数据,可以释放buffer了,于是controller的buffer数量会从7变为8。

好了,packet header的flow我们讲完了,payload header中也有一个flow位,这是什么鬼?

Lc层的流控相当于一个总阀,l2cap层以上,通过cid区分,建立了多个逻辑链路,每个逻辑链路也可能带有flow功能的,这个flow功能就是l2cap层提供的服务了,我们下一章再讲吧。

接下来,我们看看流控在btstack的体现。

对于controller向host发送的方向来说, 在收到acl data的时候:

static void acl_handler(uint8_t *packet, int size){



    // log_info("acl_handler: size %u", size);



    // get info

    hci_con_handle_t con_handle = READ_ACL_CONNECTION_HANDLE(packet);

    hci_connection_t *conn      = hci_connection_for_handle(con_handle);

    uint8_t  acl_flags          = READ_ACL_FLAGS(packet);

    uint16_t acl_length         = READ_ACL_LENGTH(packet);



    // ignore non-registered handle

    if (!conn){

        log_error( "hci.c: acl_handler called with non-registered handle %u!" , con_handle);

        return;

    }



    // assert packet is complete   

    if ((acl_length + 4) != size){

        log_error("hci.c: acl_handler called with ACL packet of wrong size %d, expected %u => dropping packet", size, acl_length + 4);

        return;

    }



#ifdef ENABLE_CLASSIC

    // update idle timestamp

    hci_connection_timestamp(conn);

#endif



#ifdef ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL

    hci_stack->host_completed_packets = 1;

    conn->num_packets_completed++;

#endif

对于host_completed_packets置1,然后会回复hci_host_num_completed_packets给controller,再把host_completed_packet置0。

对于host向controller的方向来说,当发送acl data给对端的时候,调用hci_send_acl_packet_fragments函数,其中会执行

   connection->num_packets_sent++;

将hci_connection_t 结构体的num_packets_sent加1,这个值很重要,因为在判断是否可以向controller发送数据的时候,hci_can_send_prepared_acl_packet_now中是调用hci_number_free_acl_slots_for_handle函数去判断的 

   for (it = (btstack_linked_item_t *) hci_stack->connections; it != NULL; it = it->next){

        hci_connection_t * connection = (hci_connection_t *) it;

        if (hci_is_le_connection(connection)){

            num_packets_sent_le += connection->num_packets_sent;

        }

        if (connection->address_type == BD_ADDR_TYPE_ACL){

            num_packets_sent_classic += connection->num_packets_sent;

        }

    }

    log_debug("ACL classic buffers: %u used of %u", num_packets_sent_classic, hci_stack->acl_packets_total_num);

    int free_slots_classic = hci_stack->acl_packets_total_num - num_packets_sent_classic;

根据controller报告给host的buffer大小(通过hci read buffer size command取得),减去这个num_packets_sent就是还剩下的buffer,如果buffer 为0自然就不能再发了。

当收到number of completed packets event的时候,这个num_packets_sent就可以释放。

case HCI_EVENT_NUMBER_OF_COMPLETED_PACKETS:{

            if (size < 3) return;

            uint16_t num_handles = packet[2];

            if (size != (3 + num_handles * 4)) return;

            uint16_t offset = 3;

            for (i=0; i<num_handles;i++){

                handle = little_endian_read_16(packet, offset) & 0x0fff;

                offset += 2;

                uint16_t num_packets = little_endian_read_16(packet, offset);

                offset += 2;

               

                conn = hci_connection_for_handle(handle);

                if (!conn){

                    log_error("hci_number_completed_packet lists unused con handle %u", handle);

                    continue;

                }

               

                if (conn->num_packets_sent >= num_packets){

                    conn->num_packets_sent -= num_packets;

                } else {

                    log_error("hci_number_completed_packets, more packet slots freed then sent.");

                    conn->num_packets_sent = 0;

                }

 

 

### HC-05蓝牙模块基于HAL库的源码分析 HC-05 蓝牙模块是一种常见的串口通信设备,通常用于嵌入式系统的无线数据传输。当使用 STM32 微制器及其 HAL 库开发时,可以通过 UART 接口实现与 HC-05 的通信。以下是关于如何编写和获取 HC-05 蓝牙模块基于 HAL 库的源码的相关说明。 #### 1. 基于HAL库的UART初始化配置 为了使 STM32 和 HC-05 进行通信,需要先完成 UART 外设的初始化设置。以下是一个典型的 UART 初始化代码片段: ```c #include "stm32f1xx_hal.h" void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 9600; // 设置波特率为9600bps huart1.Init.WordLength = UART_WORDLENGTH_8B; // 数据位长度为8位 huart1.Init.StopBits = UART_STOPBITS_1; // 停止位为1 huart1.Init.Parity = UART_PARITY_NONE; // 无校验 huart1.Init.Mode = UART_MODE_TX_RX; // 收发模式 huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 关闭硬件 huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); // 错误处理函数 } } ``` 此部分代码实现了 UART 的基本参数配置[^1]。需要注意的是,HC-05 默认的工作波特率可能为 9600 或其他值,在实际项目中应根据具体需求调整 `BaudRate` 参数。 --- #### 2. 数据接收与解析逻辑 在 HAL 库中,可以利用中断方式或轮询方式来接收来自 HC-05 的数据。下面展示了一个简单的中断回调函数示例: ```c uint8_t rxBuffer[20]; // 定义一个缓冲区存储接收到的数据 uint8_t index = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uint8_t receivedData = rxBuffer[index++]; // 判断是否接收到完整的命令帧或其他特定条件 if (index >= sizeof(rxBuffer)) { ProcessReceivedData(rxBuffer); // 自定义数据处理函数 index = 0; // 清零索引以便下次接收 } HAL_UART_Receive_IT(&huart1, &rxBuffer[index], 1); } } // 主程序中启动异步接收 HAL_UART_Receive_IT(&huart1, &rxBuffer[0], 1); ``` 上述代码展示了如何通过中断机制不断接收数据并将其保存到缓冲区中。一旦满足某些条件(如接收到完整的一条指令),则调用自定义的 `ProcessReceivedData()` 函数进一步解析数据[^1]。 --- #### 3. 源码资源推荐 对于初学者而言,可以从以下几个渠道寻找现成的 HC-05 配合 STM32 使用的 HAL 库源码: - **官方文档**:STMicroelectronics 提供了丰富的 HAL 库编程指南和技术手册。 - **开源平台**:GitHub 上有许多开发者分享了自己的工程实例,例如关键词搜索 “STM32 HAL HC-05” 可能找到合适的参考项目。 - **论坛社区**:像 EEWorld、CSDN 等技术交网站也经常有用户上传相关案例。 如果希望快速上手而不必完全依赖自己编码,则可以直接下载这些公开可用的例子作为起点[^2]。 --- #### 4. 注意项 在移植现有代码至个人项目前,请确认目标板子所使用的 MCU 型号以及外设管脚分配情况是否一致;另外还需注意电源电压匹配问题——HC-05 工作电平一般为 3.3V/5V,而某些微处理器 IO 输出可能是更低或者更高的数值范围,因此必要时候需加入电平转换电路保护双方器件免受损害。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值