
大家在嵌入式开发里摸爬滚打大概率都遇到过这种场景板子上的传感器数据已经能从串口正常打印了但数据只能在电脑前看人一离开工位什么都干不了。做产品原型、做现场调试、做设备监测天天被“串口线不够长”这事卡脖子。后来我换了条思路用BLE数传把串口数据接到手机App上一端是单片机串口一端是手机屏幕中间走低功耗蓝牙链路打通之后板子放哪都能看数据现场调参也不用再抱着一台笔记本到处跑了。这篇博文就完整拆一遍“串口到手机App”的BLE数传全链路从硬件选型、串口侧数据采集到BLE模块配置、手机端接收和展示每步都讲清楚“为什么这么做”穿插一些我自己踩过的坑和调试技巧。不管你是刚接触BLE的新手还是已经在用ESP32、STM32做项目的开发者按这条链路走一遍都能做出一条自己的无线数传通道。1. 完整链路设计与方案选型1.1 先看清这条链路里到底有哪几环所谓“BLE数传”剥掉各种术语本质就是一条数据搬运流水线传感器数据先由MCU采集通过UART串口交给BLE从机模块BLE从机把数据打包成广播包或GATT特征值手机App作为BLE主机扫描、连接、读取并最终显示在屏幕上。这条链路里数据形态经历了三次转换第一段是MCU串口输出的原始字节流第二段是BLE协议栈封装后的无线数据包第三段是手机App解析后的结构化数值。每一段都有自己的约束和坑比如串口侧要处理帧格式、波特率和粘包问题BLE侧要处理MTU大小、连接间隔和通知机制App侧则要先过权限、扫描、配对、服务发现这几关。我见过不少做BLE的新手第一反应是“蓝牙不就是无线串口吗”直接把串口发数据的代码套进BLE里结果要么数据长度超了被截断要么手机收不到完整帧要么好不容易连上却几秒就断开。根本原因就是没把这条链路拆开看每一段的传输特性完全不同。1.2 方案选型外挂透传模块 vs 集成SoC搭建BLE数传链路硬件上有两种主流路线一种是用MCUSTM32、Arduino等外挂一个BLE透传模块比如JDY-23、HM-10、CC2541模块MCU只当数据源BLE模块内部自带协议栈和透传固件串口进什么就无线出什么另一种是直接用带BLE的SoC比如ESP32、nRF52832这种方案MCU和BLE在同一颗芯片里开发自由度更高但代码复杂度也上来了。在实际项目里怎么选主要看两个维度一是你的数据源是不是已经固定在某个MCU上了二是你对功耗和体积的敏感度有多高。如果现有产品已经用STM32在采数据只差一个无线出口用外挂BLE透传模块是最省事的路我自己的做法是把模块当“无线串口线”用MCU代码几乎不用大改原来往串口写数据的函数直接照用。如果是从零开始做一款超低功耗的穿戴设备那就得用nRF系列SoC自己控制协议栈BLE从机连接间隔、广播间隔都能按需调把平均功耗压到微安级别。下表是我个人在选型时比较常用的对照同样参数下外挂模块胜在开发速度快SoC方案胜在灵活和性能上限高对比项外挂BLE透传模块JDY-23/HM-10集成SoCESP32/nRF52832开发周期当天就能透传至少一到两周对MCU代码改动基本不动串口函数照用要把数据管理逻辑重构到协议栈里功耗控制模块固定功耗可调空间小可精细控制连接间隔、睡眠策略调试难度低AT指令即配即用高需要熟悉SDK和协议栈适用场景现有产品加无线能力、快速原型穿戴设备、低功耗产品、大批量生产1.3 BLE和串口在逻辑上怎么对齐把BLE当成“无线串口”用有一个底层逻辑要理顺串口是一条双向管道发出去和收进来是并行的而BLE数据是按GATTGeneric Attribute Profile协议组织的数据放在服务Service里的特征值Characteristic中手机要先发现服务再读写指定特征值才能拿到数据。所以我在做链路设计时第一步就是在BLE模块上定义一个透明传输服务一个Write特征值用来接收手机下发的指令一个Notify特征值用来向手机主动推送数据。对应到MCU侧串口发过来的数据写进Notify特征值手机App写进Write特征值的内容再从串口发出去这样双向通道就建起来了逻辑上高度接近串口的工作方式。另外一个关键参数是MTUMaximum Transmission UnitBLE 4.2之前默认MTU只有23字节扣除协议头实际单包最多传20字节BLE 4.2之后可以协商到247字节。如果一次要传的串口数据超过20字节必须在固件或模块层做分包我在后面的章节里会详细讲怎么处理分包和重组。2. 硬件准备与基础配置2.1 一套最省心的硬件组合我自己调试这套链路时最常用也最推荐新手复刻的硬件组合是一块STM32F103C8T6最小系统板加一个JDY-23 BLE从机模块再加一片CP2102或CH340的USB转串口小板。整套下来成本不到三十块但能覆盖串口采集、BLE透传、手机接收这一整条路径的所有调试需求。接线方面STM32的USART1_TXPA9接BLE模块的RXDUSART1_RXPA10接BLE模块的TXD注意一定要共地GND必须连在一起否则电平参考点不一致串口数据会乱成一片。JDY-23模块支持3.3V和5V供电但串口电平是3.3VSTM32的PA9/PA10也是3.3V所以可以直接对接如果用5V的Arduino板子就需要确认模块是否兼容5V电平不兼容的话加个电平转换芯片更稳妥。选USB转串口小板时CH340和CP2102都行区别是驱动方式不一样。CH340的Windows驱动偶尔会被杀毒软件拦截CP2102相对省心但价格略高。日常调试至少准备两块USB转串口一块给STM32下载和打印日志另一块专门接BLE模块的AT配置口用来改模块参数这样能避免频繁插拔线缆少很多麻烦。2.2 串口驱动的安装与验证串口驱动这一关看起来简单实际卡住的人不在少数。CH340的驱动在Windows 10以上系统通常能自动识别但如果你用的是精简版系统或者公司电脑有驱动限制策略就得去芯片厂商官网下载对应版本的驱动别用第三方驱动精灵之类的工具容易装上全家桶。驱动装好后打开设备管理器展开“端口COM和LPT”一栏能看到一个COM口编号把USB转串口小板插到不同USB口COM口编号会变代码里或者调试工具里选的串口号要跟着改这个细节经常导致“我代码没变怎么突然连不上了”的错觉。验证驱动是否正常最直接的方法是用串口调试助手做回环测试把USB转串口板的TX和RX用杜邦线短接打开串口调试助手选对COM口和波特率发送一串字符如果能原样收到说明驱动和串口通路都没问题。这一步看似多余但能帮你排除“硬件坏了还是驱动没装好”的巨大不确定性我每次换新板子都先过一遍。2.3 BLE模块的AT指令配置与双人协作式调试外挂BLE透传模块出厂时通常默认是透传模式但波特率、设备名、广播间隔这些参数要先通过AT指令配置到项目需要的状态。以JDY-23为例先把模块的EN引脚拉高或在上电前按住模块上的按键让模块进入AT指令模式然后在串口助手里发送以下几条核心指令ATNAMEBLE_UART // 设置蓝牙从机名称方便手机端识别 ATUART115200,0,0 // 设置串口波特率115200无校验1位停止位 ATBAUD115200 // 部分模块用这条指令设置波特率 ATADVI100 // 设置广播间隔为100ms兼顾连接速度和功耗 ATMTU200 // 尝试协商更大的MTU提高单包数据量每条指令发送后模块会返回OK或者ERROR一定要等返回结果再发下一条。我习惯把AT指令配置过程写成一份清单每改一个参数就记录一次模块配置这种事特别容易“改完就忘”等过几天要复现问题时完全想不起来模块里刷了什么参数有个记录能省大量排查时间。配置完成后把模块断电重新上电进入透传模式。这时候可以用手机上的nRF Connect或者LightBlue这类BLE调试工具扫描确认能看到你设置的设备名然后尝试连接连接成功后手动向Notify特征值写入测试数据如果能在手机端读出来说明模块的BLE通道已经打通链路还剩最后一公里——怎么让MCU的数据自动进入这条通道。3. MCU固件编写从串口到BLE的稳定搬运3.1 用环形缓冲区解决串口粘包和丢包MCU串口接收数据时最经典的问题是粘包和丢包。传感器或上位机发来的一帧数据可能被拆成好几次中断接收也可能一次中断塞进来好几帧数据如果每次中断直接处理很容易出现半帧数据被当成完整帧解析的情况。解决粘包的通用方案是引入环形缓冲区串口中断只负责把收到的字节塞进缓冲区主循环或定时器任务再按帧格式从缓冲区里取数据解析。这样数据接收和数据处理被解耦中断里只做最轻量的操作不会因为处理逻辑过长导致后续字节被硬件丢弃。STM32上实现环形缓冲区核心是一个数组加两个读写索引写索引由串口中断更新读索引由解析任务更新。当写索引追上读索引时说明缓冲区满了这时候要按“丢新保旧”还是“丢旧保新”的策略处理我在数据采集场景里一般选择丢旧保新因为实时性优先过期的数据意义不大。#define BUF_SIZE 256 uint8_t rx_buf[BUF_SIZE]; volatile uint16_t head 0; volatile uint16_t tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t data USART_ReceiveData(USART1); uint16_t next (head 1) % BUF_SIZE; if (next ! tail) { rx_buf[head] data; head next; } // next tail 表示缓冲区满直接丢数据 } }这段代码里最关键的是(head 1) % BUF_SIZE这个取模操作它让数组在逻辑上变成了一个首尾相连的环。%运算在C语言里对嵌入式MCU有一定开销所以我习惯把缓冲区大小设为2的幂比如256这时取模可以用 (BUF_SIZE - 1)代替性能更好。3.2 设计一套简单的数据帧协议串口数据进了环形缓冲区后下一步是把字节流切分成一帧一帧的完整数据。我常用的帧格式是帧头0xAA 0x55 数据长度1字节 数据类型1字节 数据区N字节 CRC校验1字节 帧尾0x0A。帧头用来找帧起始位置避免把数据区里的字节误判成帧头数据长度让你知道该收多少字节才算一帧CRC校验用来过滤传输过程中被干扰的错帧。这套协议设计不复杂但能应对绝大多数数传场景比直接裸发数据可靠得多。解析时主循环每轮检查缓冲区里有没有完整的帧先找帧头再根据长度字段判断剩余数据是否够一帧够的话就把整帧搬出来做校验和解析。这样做的好处是无论串口把数据切成几段发过来最终都能正确拼出完整帧。int parse_frame(uint8_t *src, uint16_t len, frame_t *frame) { for (uint16_t i 0; i len; i) { if (src[i] 0xAA src[i1] 0x55) { // 找到帧头 uint8_t dlen src[i2]; if (i 3 dlen 2 len) return -1; // 数据还没到齐 uint8_t crc calc_crc(src i 3, dlen); if (crc ! src[i 3 dlen]) return -2; // 校验失败 memcpy(frame-data, src i 3, dlen); frame-len dlen; return i 3 dlen 2; // 返回帧尾位置外层从这里继续扫描 } } return -3; // 没找到完整帧 }这个解析函数有返回值外层调用时根据返回值决定把缓冲区里的哪些数据丢弃、哪些保留避免把半包数据直接丢掉。实际调试中我会把解析结果和原始数据同时通过另一路串口打印出来方便对比协议解析是否正确。3.3 怎么把串口数据喂给BLE模块BLE模块在透传模式下本质上就是一个无线串口管道。MCU串口收到并解析好的数据只需要继续通过另一个串口发往BLE模块模块就会自动把数据打包成BLE通知发出去。我在代码里做了一层抽象把“发送给手机”封装成一个函数例如ble_send_packet(uint8_t *data, uint16_t len)内部实现就是往BLE模块所在的串口写入数据。如果数据超过MTU限制就按MTU-3预留GATT头的长度拆成多包依次发送每包之间加一点小延时避免BLE模块内部缓冲区溢出。还有一个容易被忽视的点BLE从机模块向手机发送数据的能力和手机端的连接间隔有关。连接间隔是手机和模块之间协商的一个固定时间周期默认可能是30毫秒或50毫秒在正常情况下每个连接间隔最多能传6到8个数据包如果MQTT或传感器数据上报频率太高数据就会积压在模块内部的发送缓冲区里造成延迟越来越大。解决思路是提高MTU、缩短连接间隔或者在MCU侧做数据限流保证平均数据率低于BLE通道的实际吞吐能力。void ble_send_packet(uint8_t *data, uint16_t len) { const uint16_t mtu 200; // 应与模块AT指令配置的MTU一致 const uint16_t chunk_len mtu - 3; uint16_t offset 0; while (offset len) { uint16_t n (len - offset chunk_len) ? chunk_len : (len - offset); uart_write_bytes(BLE_UART, data offset, n); offset n; delay_ms(5); // 给BLE模块一点缓冲时间防止内部FIFO溢出 } }这段代码看起来简单但delay_ms(5)这个细节是我在实际项目里反复调出来的。刚开始我不加延时高速发送时模块偶尔丢包加了小延时后稳定性明显提升。如果你的数据量特别大可以考虑换用更高吞吐的BLE5模块或者把MTU协商到最大化。延迟与吞吐之间的平衡始终是BLE数传绕不开的核心问题。4. 手机App端开发与调试4.1 调试阶段先用现成工具验证链路写手机App之前强烈建议先用nRF Connect或LightBlue这类现成的BLE调试工具把整条链路验证一遍别一上来就写代码。手机端BLE开发最大的痛点是权限多、流程复杂如果链路本身没调通代码写得再好也没用。用nRF Connect验证时按这几个关键步骤走扫描到你的BLE设备点击Connect建立连接连接成功后看Service列表找到你定义的透明传输服务比如UUID为FFE0点进去能看到特征值列表。Notify特征值上点Enable Notifications然后让MCU定期通过串口向BLE模块发测试数据如果手机端能看到数据实时刷新说明BLE链路已经通了。这一步验证的是“硬件和模块配置是否正确”把问题范围缩小到手机端之后再写App代码才是合理的开发顺序。我见过太多人在App里调试蓝牙连接失败最后发现是模块根本没配好白折腾一整天。4.2 Android端开发的一个最小可跑流程Android平台做BLE开发从Android 6.0开始就需要动态申请定位权限Android 12及以上又细分了“附近的设备”蓝牙权限权限处理不对App连扫描都扫不到设备。我写Android端BLE功能的通常顺序是第一步申请权限。AndroidManifest.xml里声明BLUETOOTH_SCAN、BLUETOOTH_CONNECT和ACCESS_FINE_LOCATION权限运行时用requestPermissions动态申请。Android 12以上蓝牙权限属于“附近的设备”权限组需要在系统设置里单独看是否授权。第二步初始化蓝牙适配器和扫描。用BluetoothLeScanner.startScan(callback)扫描设备扫描结果回调里找到目标设备名后调用connectGatt建立连接。第三步发现服务并开启通知。连接成功后系统会回调onServicesDiscovered在这里通过getService(uuid)拿到服务getCharacteristic(uuid)拿到Notify特征值然后调用setCharacteristicNotification(characteristic, true)同时把描述符CCCD设置为ENABLE_NOTIFICATION_VALUE这样App才能收到模块主动推过来的数据。BluetoothGattCallback gattCallback new BluetoothGattCallback() { Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices(); } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { BluetoothGattService service gatt.getService(BLE_UUID_SERVICE); BluetoothGattCharacteristic notifyChar service.getCharacteristic(BLE_UUID_NOTIFY); gatt.setCharacteristicNotification(notifyChar, true); BluetoothGattDescriptor descriptor notifyChar.getDescriptor(CCCD_UUID); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] data characteristic.getValue(); // 在这里解析并更新UI } };这段代码里很多新手会漏掉写CCCD描述符这一步结果就是连接和服务都正常但设备发数据时App毫无反应。原因就在于Android系统默认不会自动使能通知必须显式往描述符里写入1才有数据上来这是个特别容易踩的坑。4.3 iOS端和跨平台方案的取舍iOS端核心是用CoreBluetooth框架流程和Android相似但权限处理简单很多只需要在Info.plist里加NSBluetoothAlwaysUsageDescription描述。iOS不允许App随意扫描所有蓝牙设备必须声明使用场景审核时也会看这条描述是否合理。如果你不想分别为Android和iOS写两套原生代码用uni-app的BLE能力是一个不错的选择。uni-app封装了uni.openBluetoothAdapter、uni.startBluetoothDevicesDiscovery、uni.createBLEConnection、uni.notifyBLECharacteristicValueChange这一系列跨平台接口一套代码两端跑。不过跨平台方案也有代价蓝牙相关的API封装层级越多出问题时越难定位是系统API的问题还是框架的问题。我个人的建议是如果是产品原型或者内部工具用uni-app能大幅提速如果是正式商业产品尤其涉及后台保活、频繁大数据量传输还是老老实实写原生稳定性和可调试性都更好。5. 常见问题排查与避坑记录5.1 串口调试时最让人上火的几类故障先说说串口侧我遇到最多的问题。第一种是电脑识别不到USB转串口设备打开设备管理器发现一个黄色感叹号。这种情况90%是驱动问题重装对应芯片的官方驱动基本能解决CH340就装CH340的CP2102就装CP2102的别混用。第二种是串口助手能打开但收发无反应。先检查接线重点看TX和RX有没有交叉连接A设备的TX接B设备的RX再看是否共地还要确认波特率、数据位、停止位、校验位两端设置完全一致。我遇到过好多次就是因为开发板默认波特率是9600而我代码里初始化成115200两边对不上数据全是乱码。第三种是串口打印乱码这通常是波特率不匹配或者代码里系统时钟配错了导致波特率实际值和理论值偏差。STM32用外部8MHz晶振配置115200没问题但如果你用的是内部HSI时钟误差可能会大到无法通信这种情况下建议把波特率降到9600试试。我把自己这几年调试串口的排查顺序总结成一套口诀式清单遇到问题照着走一遍能省大量时间先看设备管理器确认COM口存在且驱动正常再用串口助手的回环测试TX短接RX确认硬件通路然后量一下电平确认板子供电和TXD/RXD引脚电压正常最后核对参数波特率、数据位、停止位、校验位必须两端一致查模块资料确认是否有EN引脚需要拉高才能进入配置模式5.2 手机连上了但收不到数据问题出在哪手机能连接到BLE模块说明广播、连接链路都没问题但收不到数据这个问题有80%的概率出在CCCD描述符上。Android端忘了写描述符iOS端忘了用setNotifyValue都会导致这个现象。所以排查顺序一定是先确认你已经在代码里使能了Notify再用nRF Connect这类工具手动试试能不能收到数据。如果调试工具能收数据但自己的App收不到优先检查服务UUID和特征值UUID是否和模块文档里的一致很多ble透传模块默认UUID是FFE0/FFE1但不同批次固件可能改成别的。用nRF Connect直接看模块广播出来的UUID比对代码里写死的UUID这是最快的方式。另一个隐蔽的坑是Android后台限制。手机熄屏或者App切到后台后系统可能会挂起BLE回调导致数据不再刷新。这时候需要在App里申请一个前台服务或者采用高频率唤醒机制来保持接收。最简单的方式是在调试阶段把屏幕保持常亮先验证业务逻辑再考虑后台保活的技术方案。5.3 数据丢包、延迟过大这类性能问题的调试思路BLE数传的丢包和延迟问题要从两端同时排查。模块侧先看广播间隔和连接间隔是否设得太长广播间隔太长会导致手机扫描到设备很慢连接间隔太长会导致数据传输吞吐量低。我调试时习惯把连接间隔设在30毫秒到50毫秒之间这是连接速度和功耗比较平衡的区间。手机侧Android系统的BLE栈在不同厂商的ROM上表现差异很大同一套代码在小米手机上正常换到某款定制ROM上就可能丢包。这种情况没什么通用的完美解法我的经验是尽量在onCharacteristicChanged里只做轻量接收和数据入队操作UI更新放到主线程通过Handler或协程处理避免回调里做耗时逻辑导致BLE数据读取不及时。还有一点是MTU协商。如果不协商MTU默认20字节的单包上限会严重限制传输效率。建议在连接成功后主动发起MTU协商请求Android用requestMtu(200)iOS用maximumWriteValueLength(for:)查询实际支持的长度。MTU提升之后同样一次通知能传更多数据有效降低丢包率。5.4 串口烧写失败问题串口烧写失败这个话题几乎每一个玩STM32和ESP32的人都遇到过。用串口给STM32下载程序时最常见的失败提示是“芯片超时无应答”或者“连接失败”这类错误一般有四个原因BOOT0引脚没有拉到高电平、串口号选错、下载器驱动异常、或者芯片已经处于读保护状态。STM32的串口下载流程是先拉高BOOT0复位芯片进入系统存储器Bootloader模式然后通过USART1的串口接收程序镜像。很多人烧写失败是因为没复位或者BOOT0跳线帽没接对。我用的是带BOOT0按钮和复位按钮的最小系统板操作顺序是按住BOOT0按一下复位松开BOOT0然后开始下载。ESP32的串口烧写失败相对复杂一些通常是因为下载时GPIO0必须保持低电平进入下载模式。ESP32开发板一般内置了自动下载电路但如果用的是自己搭的模组就得手动把GPIO0接地再上电。另外供电不稳也会导致烧写失败USB线用了劣质线或者USB口供电不足都会出现“连接失败”或“芯片超时”的报错换一根短而粗的数据线往往就正常了。5.5 我的调试辅助工具清单最后分享一套我固定在用的调试工具组合每次做BLE数传项目都靠它们省掉大量无效排查时间硬件方面逻辑分析仪是排查串口波形问题的利器任何“代码看起来对但就是不通”的串口疑难杂症接上逻辑分析仪看一眼波形就能定位。USB转串口板至少备两块一块给MCU调试日志一块给BLE模块AT配置。电压表用来查供电和电平问题。软件方面nRF Connect和LightBlue是手机端BLE调试标配串口调试助手我常用sscom和MobaXterm前者登录串口调试简单直接后者用它的串口会话直接看日志。如果是用来抓手机和BLE模块之间的通信包有预算可以上PCAP格式的BLE嗅探器没有就用nRF Connect的记录功能也能做个大概分析。写在最后的一点体会这整套“串口到手机App”的BLE数传链路开发过程中最大的体会其实不是某个具体技术有多难而是每一层的坑都很隐蔽串口侧是电平适配和帧格式模块侧是MTU和连接参数手机侧是权限和描述符任何一环没对齐整条链路就是不通的。按照“先硬件、再模块、再工具验证、最后写App代码”的顺序一步步推进是最稳的一条路。再分享一个小技巧整套链路里我花最多时间排查的往往是那些“看起来最简单”的环节比如串口号变了、接线松了、模块默认波特率和你代码不一致这类低级问题反而比协议栈还难发现。所以做BLE数传建议从一开始就准备一张调试记录表每次配置和测试都往里填数据后面排查问题时能省下几小时的时间。链路打通之后再回头优化功耗、吞吐和稳定性那就是另一个话题了但底层的这套经验到哪个项目都通用。