尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

BlueNRG ATT_MTU修改指南:吞吐量提升10倍的实战解析

BlueNRG ATT_MTU修改指南:吞吐量提升10倍的实战解析 我前阵子刚把一个基于BlueNRG-2的设备从固件升级要15分钟调到了1分半包完瓶颈就出在ATT_MTU一直沿用SDK默认值23字节。BlueNRG系列这几年在蓝牙低功耗市场占有率不低ST官方SDK虽然封装得很完善但ATT_MTU这个参数藏在初始化链路深处很多项目直接用官方Demo的默认配置结果就是数据吞吐量被死死压在小包传输的档位上透传、OTA、传感器批量上报全受影响。这篇文章把BlueNRG SDK里修改ATT_MTU的完整链路拆开讲一遍包括API怎么调、事件怎么接、主从两种角色各自该做什么以及我实际调试中踩过的几个坑。无论你用的是BlueNRG-1/2还是BlueNRG-LP只要搞懂这套协商机制都能很快把吞吐量拉上去。1. 为什么默认只能传20字节ATT_MTU的由来与性能瓶颈1.1 ATT层和L2CAP层的关系以及MTU到底卡在哪先理清一个容易混淆的概念很多人说BLE一次只能发20字节严格讲这是ATT层默认MTU限制不是链路层的物理限制。BLE协议栈从上到下依次是应用层、GATT/ATT层、L2CAP层、链路层和物理层。GATT层管理的是属性——也就是特征值、服务、描述符这些概念。真正在空口上传输时一个ATT PDU要装进L2CAP的帧里再交给链路层拆包发送。而这里所谓的ATT_MTUAttribute Maximum Transmission Unit指的是ATT层一次能处理的PDU最大值默认情况下是23字节。这23字节的构成非常紧凑字段长度字节ATT Opcode操作码1Attribute Handle属性句柄2Attribute Value有效载荷20也就是说真正留给业务数据的只有20字节。整包23字节再往下传给L2CAP后L2CAP还要加上自己的4字节头所以实际上链路层单个数据包的空中长度是27字节这是BLE 4.0/4.1时代就已经固定的基础设计。1.2 20字节的由来以及它怎么变成项目的绊脚石早期BLE的目标场景是遥控器、手环、标签这类低功耗小数据应用一颗纽扣电池要撑一年甚至更久协议栈的接收缓冲、发送缓冲和内存占用都必须抠到极致。20字节的有效载荷在这种场景下是够用的——心率、步数、温湿度哪个都超不出20字节。但后来应用场景变复杂了OTA固件升级动辄几百KB、音频透传、高速传感器数据流甚至配置类报文的JSON结构都有几十字节。20字节一包意味着什么以100KB固件包为例20字节一包要拆51200包每包还有协议头、确认等待、连接事件间隔效率低到让人抓狂。好在BLE 4.2之后协议加入了MTU协商机制同时物理层也支持了DLEData Length Extension和更高的速率。MTU协商让两端可以约定一个更大的ATT包体积而DLE则进一步把链路层的单包长度从27字节扩展到251字节——这两者配合起来才是现代高速BLE传输的基础。1.3 MTU协商机制谁发起、谁响应、结果怎么算MTU协商的核心规则是GATT Client通常是手机或中心设备发起Exchange MTU Request携带自己希望使用的MTU值GATT Server通常是被连接的外设收到后返回自己的MTU值最终协商结果取两者中的较小值。要注意一个关键点Server不能主动发起协商。如果外设的max_att_mtu初始化值很小而Client想用大MTU结果会被外设的值卡住。反过来也一样如果外设初始化时设置了247但手机Client从没发起过Exchange MTU Request那协商流程压根不会发生MTU始终保持默认23。这也是后面很多项目改了没生效的根源。协商完成后两端各自记住这个值后续所有ATT PDU都按新MTU走。协商值只对当前连接有效断线重连后需要重新协商如果设备和多个中心设备同时建立了连接每条连接的协商结果也是独立的。2. BlueNRG SDK里那几个跟MTU有关的API到底各自管什么2.1 SDK版本和API命名差异先看清楚BlueNRG系列分两个大的SDK分支BlueNRG-1/2用的是传统的STSW-BNRG1/2 SDKAPI以aci_开头事件以EVT_BLUE_开头BlueNRG-LP用的是更新的STSW-BNRGLP SDK或X-CUBE-BLE1接口命名和事件结构体都发生了变化。下文主要基于BlueNRG-1/2的SDK 2.5.x/2.6.x来写如果你用的是BlueNRG-LP功能模块是一致的但具体函数名、事件结构体字段要对照你那份SDK的头文件核对。别指望代码能一字不差跨分支拷贝先确认API名称再改。2.2 三个核心角色初始化参数、协商请求、协商事件与ATT_MTU强相关的也就三个东西搞清楚它们的关系剩下就是填参数的事。角色API / 事件作用GATT初始化的上限值aci_gatt_init(max_att_mtu)配置协议栈愿意支持的MTU上限主从设备都要调发起协商aci_gatt_exchange_configuration(connection_handle)GATT Client连上后主动发出协商请求协商结果回调EVT_BLUE_GATT_ATT_MTU_CHANGE携带实际协商后的MTU值返回给应用层aci_gatt_init是整套机制的地基。你把这个参数改成247相当于告诉协议栈我的接收缓冲区最大能装247字节的ATT PDU。如果保持默认23那协商请求即使发出去了对端也会用23把你的请求压回去。aci_gatt_exchange_configuration是客户端发起的动作注意它接收的参数是connection_handle不是链路层专属的handle而是GAP层连接完成事件里带出来的那个连接索引。很多人在这一步传错句柄或者干脆没用连接完成事件里对应的值导致函数直接返回错误。EVT_BLUE_GATT_ATT_MTU_CHANGE是协商完成的回调事件一般放在事件分发函数里和GATT其它事件一起处理。它返回的mtu字段就是我们后续分包发送时的重要依据。2.3 另一个容易被忽略的配置文件入口除了运行时APIBlueNRG SD常会提供一个协议栈配置头文件比如ble_config.h或link_layer_config.h里面有些和缓冲区分配、GATT最大属性数相关的宏。部分SDK版本里MAX_ATT_MTU还需要在编译期通过宏定义预分配一定资源光在代码里调aci_gatt_init(247)但编译期宏没放开协议栈内部的buffer还是按旧值分配的表现起来就是协商失败或者运行一段时间后内存异常。检查顺序建议先看头文件宏定义再确认aci_gatt_init参数最后查调用顺序——这个顺序调反了会出现各种隐性失败。3. 从默认23到247完整修改流程与关键代码3.1 服务端GATT Server配置把地基打大不管你的设备是当外设还是当中心GATT初始化这一步都必须把max_att_mtu调大。对绝大多数跑BlueNRG-1/2的板子来说在main函数或者协议栈初始化函数里找到这行/* GATT层初始化 */ uint8_t ret aci_gatt_init(247);很多官方例程这里写的是23这是默认值改了就生效。但有一点需要特别提醒必须确认aci_gatt_init在aci_gap_init之后调用并且在添加任何服务、开始广播之前完成。如果服务已经注册进协议栈再改这个参数部分SDK版本会直接忽略新值因为GATT表和缓冲区已经按旧值建好了。如果你用的是BlueNRG-LP的新SDK初始化函数名可能是aci_gatt_initialize或者通过BLE_Stack_Init这类总入口间接配置同样原则适用——在连接建立之前、在GATT服务注册之前把上限设好。如果你的项目里同时存在多个连接或者你还用了GATT缓存、链路层加密等特性aci_gatt_init(247)分配的内存会对比默认值略大需要评估一下MCU的RAM余量。BlueNRG-1的RAM只有84KB如果固件模块很多别把缓冲开得太大247一般没问题512就要谨慎了。3.2 客户端GATT Client发起协商连接完成后的第一时间外设作为Server只能等Client来协商但如果你自己用BlueNRG当中心设备、连接手机/其他Peripheral那发起协商的动作就是在你这边。在GAP连接完成事件处理代码里加上协商调用case EVT_BLUE_GAP_CONNECTION_COMPLETE: { uint16_t conn_handle event-evt.blue_evt.gap_evt.connection_handle; /* 连接建立成功后马上发起MTU协商 */ aci_gatt_exchange_configuration(conn_handle); } break;这里有个时序细节aci_gatt_exchange_configuration必须在连接已经稳定建立、并且通过GATT认证如果需要加密之后才能调用。如果你的应用层在收到连接完成事件后马上又发起了配对流程要注意配对和MTU协商的先后顺序。有的协议栈版本在处于配对状态时调用MTU协商可能被延迟处理表现为事件迟迟不来——建议要么先配对再协商要么在配对完成后回调里根据需求重新触发一次。简单粗暴的做法是在EVT_BLUE_GAP_CONNECTION_COMPLETE和配对完成的回调里都调用一次aci_gatt_exchange_configuration但用标志位避免重复触发。协议栈在已有协商结果的连接上重复发起时行为可能因版本而异最好用标志位挡住。3.3 事件回调里读取协商结果协商完成后协议栈会上报EVT_BLUE_GATT_ATT_MTU_CHANGE事件结构体里带一个mtu字段case EVT_BLUE_GATT_ATT_MTU_CHANGE: { uint16_t mtu event-evt.blue_evt.gatt_evt.att_mtu_change.mtu; app_state.att_mtu mtu; app_state.mtu_ready 1; } break;这个mtu就是当前连接实际生效的MTU值也是你后续计算单次数据包最大长度的唯一依据。强烈建议把这个值保存到全局状态结构体里并加一个mtu_ready标志位。原因下面会说。3.4 发送数据一侧的适配别超MTU必要时自己分包MTU协商完成不等于万事大吉。对BlueNRG-1/2来说aci_gatt_update_char_value、aci_gatt_send_notification这类发送通知/指示的API在单次发送时实际写入的val_len受到当前连接ATT_MTU约束。如果你传了一个超过当前MTU - 3的长度协议栈会返回一个参数错误。正确的发送策略是/* MTU为247时单次notification最大有效载荷为247-3244字节 */ uint16_t max_payload app_state.att_mtu - 3; /* 如果待发送的数据长度超过max_payload循环分包发送 */ uint16_t offset 0; while (remaining 0) { uint16_t chunk (remaining max_payload) ? max_payload : remaining; memcpy(ntf_buffer, data offset, chunk); aci_gatt_update_char_value(serv_handle, char_handle, 0, chunk, ntf_buffer); offset chunk; remaining - chunk; }分包逻辑要放在mtu_ready标志位为1之后执行。如果应用层上来就发大块数据而协商还没完成att_mtu还停留在23那就只能按20字节分包效率低不说还有可能因为数据切分逻辑混用导致前后包长度不一致。4. MTU协商不生效的四种典型场景我全踩过4.1 场景一服务端把aci_gatt_init(247)改成247了为什么还是20字节一包这个问题的概率最高。很多开发者误以为外设端把初始化参数调大整个连接的MTU就会自动变大。但实际上MTU协商必须由GATT Client发起外设只能声明自己支持大MTU却不能主动发起协商。如果你用手机调试工具连接设备打开某款BLE调试APP默认情况下它未必会自动发起MTU协商。如果APP没有相关设置连接后设备始终保持23字节这在手机端是正常的。换用nRF Connect这类工具在连接面板里手动点击Request MTU并填成247数据包立刻变大——这就能验证设备端其实已经配置正确问题出在发起方。我的排查顺序是先确认调试工具是否主动发起协商再用真正的应用代码复现最后才回头检查初始化参数。4.2 场景二协商完成了但最终MTU只有23或者一个奇怪的中间值这种情况通常是两端支持值取交集后的结果。比如手机端请求247但设备的aci_gatt_init(23)协商结果就是23。反过来设备初始化了247但手机端只请求了180协商结果就是180。还有一个隐藏点不同SDK版本对aci_gatt_init参数的最大值有限制。有些旧版BlueNRG-1协议栈内部对MTU上限有硬限制比如最大247你传512进去可能被强制截断或者返回错误。改大参数之前去ble_gatt.h或aci_gatt.h里查一下这个参数是否有宏定义约束别盲试。4.3 场景三事件回调里读到的MTU值对但一调用发送大包API就报错我之前在给BlueNRG-2写OTA下载逻辑时遇到一个诡异现象协商事件里读到了247可紧接着调用aci_gatt_update_char_value发送200字节依然返回ERR_INVALID_PARAMETERS之类的错误。排查半天发现问题出在目标特征值的属性长度char_prop、max_value_len配置上。GATT服务定义时每个特征值都有max_value_len参数它限定了这个特征值属性最大能持有的字节数。如果建服务表时把max_value_len设成了32字节即使MTU协商到247写入200字节也超过了特征值属性本身的长度限制。修改方法是在添加特征值的服务表结构体里把对应特征值的最大长度同步调大比如设为512或更大。这是BlueNRG SDK里特别容易踩的一类问题MTU和特征值长度是两个维度MTU定了单包能传多少max_value_len定了这个属性整体能存多少。两个都要满足。4.4 场景四Android/iOS手机端表现不一致有的能上247有的卡在185手机端同样有各自的MTU策略这对量产产品的兼容性测试很重要。Android从5.0开始支持BluetoothGatt.requestMtu(int mtu)应用可以主动请求247甚至更大的值。但Android 6.0之前部分机型对requestMtu支持不完善可能请求完还是23。Android 8.0以后大部分设备都能协商到247部分新机型还能到512。iOS则比较特殊CoreBluetooth没有公开的MTU协商API系统在连接后会自动和外围设备协商一个对双方都合适的MTU通常对iOS 10以上设备如果外围设备支持协商结果会在185字节左右也有一些场景下是247。iOS给应用层的maximumWriteValueLength(for:)直接返回这个值你不需要也不能去主动触发协商。所以做双端产品时外设侧不要假设MTU一定等于247而是把协商结果当作运行时状态动态调整发送分包和接收缓冲。建议协商完成后完整走一遍收到发起方的协商请求→用设备侧上限值返回→再按返回的MTU发一个回包确认保证两端的实际分包逻辑一致。5. 实测数据说话不同MTU下的吞吐量差距有多大5.1 同连接参数下的横向对比我在BlueNRG-2开发板上做了一组简单实测连接间隔设成7.5ms每个连接事件最多能发6个包物理层用1M PHY分别统计MTU为23、63、247三种配置下每秒能推送多少字节纯数据有效载荷。协商MTU单包有效载荷理论每秒有效数据满打满算实测均值2320字节约 1000/7.5 × 6 × 20 ≈ 16000 B/s约 14 KB/s6360字节约 1000/7.5 × 6 × 60 ≈ 48000 B/s约 43 KB/s247244字节约 1000/7.5 × 6 × 244 ≈ 195200 B/s约 175 KB/sMTU从23提到247吞吐量提升了10倍以上。这个提升量级对OTA、日志回传、批量数据采集都是决定性的。5.2 MTU不是越大越好内存和重传成本要算进去既然247这么好那干脆设512行不行行是行但有两个代价要仔细权衡。第一个代价是内存。MTU每增加一个字节链路层接收缓冲就要多预留对应空间。247的ATT_MTU要求链路层单包最多承载255字节算上L2CAP和LL头如果此时再用DLE把链路层包长也扩到251字节接收缓冲至少多预留几百字节空间。对于BlueNRG-1这种RAM紧张的芯片一个连接开512字节缓冲还好如果同时连两个或者三个中心设备RAM开销翻倍后很容易挤占应用层堆栈和GATT属性区。第二个代价是重传成本。空中还是存在干扰、丢包的一个包越大丢失后重传浪费的空口时间就越多。实测下来在信号较差的环境里247的MTU误包率比23高一点但总吞吐量依然明显占优。真正需要警惕的是超大数据包在Socket重传、CRC不过导致的重复请求对某些不稳定链路从247降到185反而总吞吐量更稳。5.3 和连接间隔、DLE、2M PHY的配合关系MTU只是决定单包容量的因素之一。实际吞吐量的公式大约是吞吐量 ≈ (每个连接事件能发包数 × 单包有效载荷) ÷ 连接间隔想突破更高的数据量需要同时做四件事把连接间隔从30ms缩短到7.5ms把每个连接事件里可发送的包数拉满受链路层调度和对方处理速度影响开启DLE把链路层单包从27字节扩展到251字节条件允许时用2M PHY物理速率翻倍后再配合大MTUBlueNRG-2支持BLE 5.0的2M PHY和DLE做完这些之后在我的测试环境里能达到400-500 KB/s级别的吞吐和1M PHY下MTU 23的十几KB/s完全不是一个量级。但对大多数固件升级、传感器回传场景247的ATT_MTU配合标准1M PHY已经能覆盖需求优先把MTU稳住再循序渐进上2M PHY和DLE不要一上来就全改。我个人在实际项目里的做法是外设端aci_gatt_init(247)中心设备连接后主动发协商请求协商完成后用返回的att_mtu动态计算分包长度同时把特征值的max_value_len也同步设置成和MTU配套的值。这样不管是连iOS还是Android不管是1M还是2M PHY都能在协议栈能力范围内尽量跑满带宽。做过一轮设备端升级、双端联调、弱信号稳定性测试之后这套MTU配置基本不再需要改动。如果后续你再把DLE打开记得先把这里的分包长度重新确认一遍不要拿旧参数直接套。
返回列表