iOS Swift BLE UART问题:didUpdateValueFor仅触发两次后停止
我之前踩过几乎一模一样的BLE UART坑——外设每隔一段时间发数据,iOS连好后只收几次就停了,后来排查发现核心是BLE的流量控制机制没处理到位,要么是iOS没给外设“已接收”的确认,要么是外设没等确认就硬发导致缓冲区堵死。结合你的场景(发单个'0'、250ms间隔),给你几个针对性的解决步骤:
iOS端排查与修复
1. 收到数据后务必回复外设确认信号
大多数BLE UART实现(比如Nordic的标准UART服务)都要求中心设备(iPhone)在收到数据后,主动向外设的RX特征写入一个确认(可以是空数据,或者特定字节),告诉外设“我收好了,继续发”。如果没做这一步,外设的发送缓冲区会很快被占满,或者iOS的CoreBluetooth内部缓冲区满了就会暂停通知。
举个Swift代码示例(假设用的是Nordic标准UART UUID):
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard error == nil, let data = characteristic.value else { print("接收数据出错: \(error?.localizedDescription ?? "未知错误")") return } // 这里处理你的数据解析(比如打印收到的'0') if let receivedStr = String(data: data, encoding: .utf8) { print("收到数据: \(receivedStr)") } // 关键:找到UART的RX特征,写入确认信号(空数据即可) if let uartService = peripheral.services?.first(where: { $0.uuid == CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E") }), let rxCharacteristic = uartService.characteristics?.first(where: { $0.uuid == CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E") }) { // 用withoutResponse类型,避免额外的ACK开销 peripheral.writeValue(Data(), for: rxCharacteristic, type: .withoutResponse) } }
2. 确保通知订阅状态稳定
有时候didUpdateValueFor停止触发,是因为通知被意外取消了。你可以在didUpdateNotificationStateFor里检查状态,确保订阅成功:
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { guard error == nil else { print("通知状态更新失败: \(error!.localizedDescription)") return } if characteristic.isNotifying { print("UART通知已开启") } else { print("UART通知被关闭,重新订阅") peripheral.setNotifyValue(true, for: characteristic) } }
外设端排查与修复
1. 实现“发完等确认再发”的流量控制
你现在是每隔250ms硬发一个'0',但BLE是半双工通信,外设不能一直发而不管中心设备是否接收完毕。正确的逻辑应该是:
- 发送一个'0'后,暂停定时器
- 收到iOS发来的确认信号(也就是刚才写RX特征的操作)后,再重启定时器发送下一个'0'
举个伪代码例子(以Nordic SDK为例):
// 定时器回调:发送单个'0' static void timer_timeout_handler(nrf_timer_event_t event_type, void * p_context) { uint8_t send_data = '0'; // 发送数据 ble_uart_string_send(&m_uart, &send_data, 1); // 发送后禁用定时器,等待iOS的确认 nrf_drv_timer_disable(&m_timer); } // UART事件回调:处理iOS的确认 static void uart_event_handler(ble_uart_t * p_uart, ble_uart_evt_t * p_evt) { switch(p_evt->evt_type) { case BLE_UART_EVT_TX_COMPLETE: // 收到iOS的确认,重启定时器发下一个'0' nrf_drv_timer_enable(&m_timer); break; case BLE_UART_EVT_RX_DATA: // 这里可以处理iOS发来的其他数据,确认信号可以是空数据,所以不用额外解析 break; default: break; } }
2. 检查外设的发送缓冲区配置
如果外设的BLE发送缓冲区太小,连续发数据会导致溢出。可以查看你的嵌入式SDK文档,调整缓冲区大小,或者限制单次发送的数据量(你这里发1字节,问题不大,但缓冲区还是要足够)。
额外优化:协商更大的MTU
默认BLE MTU是23字节,虽然你每次只发1字节,但协商更大的MTU(比如512)可以提升传输效率,减少缓冲区压力。iOS端可以这样请求:
func peripheral(_ peripheral: CBPeripheral, didConnect error: Error?) { if error == nil { peripheral.discoverServices(nil) // 协商MTU peripheral.requestMTU(512) } }
外设端也要开启MTU协商支持,具体配置参考你的SDK文档。
按照上面的步骤调整后,应该就能建立稳定的UART连接,持续接收外设发来的'0'了。
内容的提问来源于stack exchange,提问作者pholowko

