做嵌入式这些年,最常被问到的需求就是“能不能让设备接上手机”。BLE(低功耗蓝牙)确实是答案,协议栈也不用真从零啃,但要在项目规定时间内拿出能演示的固件,直接在MCU上裸调BLE还是太靠运气。去年做一款便携式空气质量仪时,我直接选了E104-BT02这颗BLE透传模块,把射频、协议、天线全部外包给模块,主控侧只通过串口读写数据,从画板到手机App收到第一包数据,整个过程快得超出预期。
这篇文章把整套方案拆开讲,包括开源电路怎么搭、基于HAL库的驱动代码怎么设计、手机蓝牙调试助手怎么完成绑定(Bond)和透传测试,以及那些在协议栈层面真正容易踩的坑。项目资料里的原理图和驱动代码都是可复用的,照着接线改个串口就能跑。
1. 为什么选E104-BT02这种透传模块而不是自己写协议栈
1.1 在MCU上直接做BLE的代价有多大
如果手里是一颗不带BLE的普通MCU(比如STM32F103、STM32F407),要加蓝牙功能通常有两条路:换一颗带BLE的SoC,或者外挂一颗透传/数传模块。
我见过不少工程师选了第一条路,结果在协议栈上耗了一两个月。BLE协议栈虽然比蓝牙经典简单,但也要处理GAP层、GATT层、L2CAP层、安全管理层,光是搞清楚广播参数和连接间隔对功耗的影响,就得翻不少文档。更麻烦的是,方案选型时要考虑芯片供应是否稳定、SDK有没有长期维护、模块射频能不能过认证。这些确认完,真正写业务代码的时间反而被挤没了。
所以只要业务不需要自定义太深的BLE行为,我都建议走外挂模块这条路。E104-BT02这类透传模块把芯片选型、协议栈移植、射频调试、天线设计都解决掉了,MCU工程师只需要把它当成一个“无线串口”:串口发出什么,远端就收到什么;远端发来的数据,本地串口就能读到。
1.2 模块硬件资源和适用场景
E104-BT02是单模BLE透传模块,工作在2.4GHz频段,串口波特率从9600到115200都可以配置,默认常见的是115200。模块工作电压1.8~3.6V,3.3V供电可以直接怼,不用额外电平转换。内部的BLE SoC已经处理好晶振、射频匹配和天线阻抗,我们只需要供电和接UART。
数据吞吐量方面,BLE本身设计成低功耗低速率,透传场景下稳定跑几Kbps到几十Kbps没问题,传传感器数据和控制指令绰绰有余。我实际用它做过三个场景:
- 工业采集器:现场传感器通过RS485汇总到主板,主板把计算后的数据通过E104-BT02发给旁边的平板,距离二十米内,隔两三堵墙,连续跑一周没有丢包。
- 便携式空气质量仪:MCU每秒采集一次PM2.5和温湿度,通过BLE发给手机App刷新界面,电池供电,模块在未连接状态下的功耗很低。
- 智能门锁:手机通过BLE透传做配网和临时密码下发,这种突发小数据量场景对BLE来说非常轻松。
如果你要做的是音频传输、大文件传输这类高速率需求,请直接选蓝牙经典或WiFi,BLE透传模块不合适。这个先认清,后面就不会纠结“为什么速度上不去”。
1.3 BLE与蓝牙经典的区别,以及广播类型
刚接触蓝牙的人经常把BR/EDR(蓝牙经典)和BLE搞混。平时听歌用的蓝牙耳机大多是BR/EDR,支持音频流,速率高,但功耗大、配对流程复杂。BLE则相反,物理层和链路层按低功耗设计,单次连接传的数据不大,但功耗可以压得很低,移动端接入也更简单粗暴。用生活化比喻就是:BR/EDR像货车,BLE像自行车,要拉货选货车,要在小区里通勤选自行车。
广播类型也会影响连接体验。常见广播类型有:
- 可连接无向广播(ADV_IND):设备主动广播,手机能扫到也能发起连接,这是透传模块最常用的类型。
- 定向广播(ADV_DIRECT_IND):指定某个设备可以连接,场景是快速重连,一般用于已知对端地址的情况。
- 不可连接广播(ADV_NONCONN_IND):只往外发广播数据,不允许连接,常见于信标(Beacon)场景。
- 可扫描广播(ADV_SCAN_IND):允许主动扫描请求,但同样不能建立连接。
透传模块默认一般用ADV_IND,手机才能扫到并连接。如果哪一天你发现手机能扫到模块但怎么也连不上,先去看它是不是被配置成了不可连接广播。我就犯过这个低级错误,调了半天才发现是之前测试信标模式时改错了参数。
2. 开源电路设计与最小硬件系统
2.1 引脚定义和接线参考
E104-BT02的引脚不多,典型应用下用到6个左右。下面是开源电路里最核心的接线关系,可以直接抄:
| 模块引脚 | 方向 | 连接目标 | 说明 |
|---|---|---|---|
| VCC | 电源输入 | 3.3V | 靠近引脚放0.1uF+10uF退耦电容 |
| GND | 电源地 | 系统地 | 走线尽量短粗 |
| TXD | 输出 | MCU_RX(如PA3) | 模块发给MCU |
| RXD | 输入 | MCU_TX(如PA2) | MCU发给模块 |
| SET/CFG | 输入 | 任意GPIO(如PB0) | 高电平进AT配置模式,低电平进入透传 |
| AUX/STA | 输出 | 可选GPIO或LED | 连接成功后拉高,可做状态指示 |
这里有个容易忽略的点:SET/CFG引脚不能悬空。如果悬空,模块可能因引脚电平不确定而进入错误模式,导致上电后既不广播也不响应AT指令。我会在模块和主控之间串一个10kΩ下拉电阻,确保默认进入透传模式,只有需要配置时才把引脚拉高。
另外,模块串口的TXD/RXD连接主控串口时,要注意是交叉连接。TXD接主控的RX,RXD接主控的TX,接反了就会一直收不到数据。很多时候第一次调试模块没反应,先检查的就是交叉关系。
2.2 电源、天线和PCB布线细节
E104-BT02对电源不算苛刻,但BLE在广播和连接瞬间电流会有明显波动,如果供电纹波太大,可能会出现“能进AT模式但连不上手机”这种奇怪问题。我习惯给模块单独加一颗LDO(耐压足够的低压差线性稳压器),输出3.3V,后面并上10uF钽电容和0.1uF陶瓷电容。如果主控和模块共用同一路3.3V,至少要在模块VCC引脚附近加退耦电容,别让电机、继电器这类大电流器件的浪涌串进来。
天线部分是BLE模块最容易踩坑的地方。模块自带PCB天线,天线周围要留出净空区域,正下方不要铺铜,不要走信号线。金属外壳、大块地平面、电池都尽量远离天线,否则辐射效率下降,通信距离会肉眼可见地缩短。样机上就吃过一次亏,天线旁边放了一颗金属封装的晶振,结果隔一堵墙就丢包,挪走之后同一个位置能稳定穿两堵墙。
如果模块要用在强电磁干扰环境,可以在电源输入端串一颗磁珠(比如600Ω/100MHz),再配合电容组成π型滤波。实际量产中这个设计对EMC测试帮助很大。
2.3 开源电路里我额外加的东西
开源电路除了最小系统,我还加了几个实用外设:一颗I2C接口的0.91寸OLED用来显示模块连接状态、蓝牙信号强度(RSSI)和收发计数;一组0.36寸3位数码管用来显示当前连接的设备数或最近的RSSI数值。这两个显示设备平时调试非常方便,不用频繁打断代码去看日志。
OLED和数码管驱动都是基于HAL库写的,OLED用I2C,数码管用GPIO扫描或专用驱动芯片。如果你手头也有LIS2DW12这类加速度传感器,或者TM1621这类LCD驱动芯片,代码风格保持一致,直接在main.c里加对应模块就行。这里的核心思想是:E104-BT02只是一个串口外设,它不应该影响主控上其他外设的驱动架构。
3. 驱动代码实操:从HAL库到透传数据流
3.1 代码结构和初始化流程
开源驱动代码按功能拆成几个文件:ble_uart.c负责E104-BT02的串口收发,buffer.c实现环形缓冲区,oled.c和segment.c负责显示,main.c做初始化调度。这样的分层很清晰,以后换主控平台,只需要改ble_uart.c底层的串口句柄。
初始化顺序很关键,我建议按下面的步骤:
- 初始化系统时钟和串口GPIO(PA2/PA3对应USART2)。
- 初始化E104-BT02的SET/CFG引脚为输出,先拉低,确保模块进入透传模式。
- 初始化USART2,波特率设为115200,8位数据,无校验,1位停止位。
- 打开串口接收中断,每收到一个字节就写入环形缓冲区。
- 最后才初始化OLED和数码管,因为显示内容要反映通信状态。
串口初始化部分用STM32CubeMX生成也可以,但要记得在MX_USART2_UART_Init()之后补上接收中断的启动代码。CubeMX默认不会自动开启接收中断,少了这一步后面所有数据都进不来。
3.2 发送数据的关键代码
E104-BT02的透传逻辑很简单:往串口写什么,远端就收到什么。发送函数可以直接封装成下面这样:
void BLE_SendData(uint8_t *data, uint16_t len) { HAL_UART_Transmit(&huart2, data, len, 100); }这个函数依赖HAL_UART_Transmit的阻塞模式,对大多数透传场景够用了。但要注意,如果发送的数据量很大,阻塞时间太长会影响主循环其他任务。实际项目中我建议加一个互斥信号量或简单标志位,避免发送和接收中断同时操作同一个缓冲区时出现竞争。
另一个经验:不要在串口中断回调里直接调用HAL_UART_Transmit。中断中做耗时操作容易导致优先级反转或丢数据,把要发送的数据放到队列里,在主循环中统一处理更稳。
3.3 接收链路和环形缓冲区
接收链路是驱动代码的重点。最简单的做法是每收到一个字节就进中断,把数据写入环形缓冲区,主循环再去解析。下面是一段典型的实现:
#define RX_BUFFER_SIZE 256 uint8_t rx_byte; uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint16_t rx_write_index = 0; volatile uint16_t rx_read_index = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { rx_buffer[rx_write_index] = rx_byte; rx_write_index = (rx_write_index + 1) % RX_BUFFER_SIZE; HAL_UART_Receive_IT(&huart2, &rx_byte, 1); } }上面代码每次重新调用HAL_UART_Receive_IT,实现单字节持续接收。读取数据时判断读写索引是否相等:
uint16_t BLE_GetDataLength(void) { return (uint16_t)((rx_write_index + RX_BUFFER_SIZE - rx_read_index) % RX_BUFFER_SIZE); } uint8_t BLE_ReadByte(void) { uint8_t c = rx_buffer[rx_read_index]; rx_read_index = (rx_read_index + 1) % RX_BUFFER_SIZE; return c; }环形缓冲区的大小建议至少256字节。因为BLE连接事件是周期性的,数据可能会一批一批到达,缓冲区太小容易丢帧。如果连续传比较大的数据包,可以考虑把缓冲区扩到512或1024。
从手机端收到数据后,上层还要做帧协议处理。我不建议直接把串口原始数据当作完整业务数据进行解析,至少要加帧头、长度、校验三个字段。BLE传输链路本身有CRC,但透传模块分包后,应用层收到的数据边界不一定是发送方的边界,所以业务侧必须自己处理粘包和半包问题。
3.4 显示联动:OLED和三位数码管
开源代码里,OLED显示的是模块实时状态,包括:连接状态(IDLE/CONNECTED)、最近一个RSSI值、累计接收字节数。OLED的HAL库驱动代码就是标准的I2C OLED驱动,初始化SSD1306控制器,每次刷新时把状态字符转换成点阵缓存,再通过I2C发送。
0.36寸3位数码管我用的是TM1650这类I2C接口驱动芯片,或者直接用普通SEG引脚扫描。代码里做了一个Segment_DisplayRSSI(int8_t rssi)函数,显示范围是-100到0,负号用横杠表示。调试时眼睛瞄一眼数码管就知道当前信号好不好,不用每次掏手机。
有人可能会觉得显示设备和BLE模块放在一起有点多余,但实际开发中,一个能随时看RSSI的硬件调试端口,比任何打印日志都直观。项目现场没有电脑的时候,数码管上的数字就是最好的调试信息。
4. BLE连接、绑定与调试实战
4.1 用手机蓝牙调试助手完成绑定(Bond)
模块上电后,用手机上的BLE调试助手扫描,会出现一个默认名字的设备。点击连接后,有些模块会要求配对绑定。绑定(Bond)和普通连接不同,配对之后,手机和模块会交换并保存长期密钥,下次连接时不再需要重新输入PIN码。
我常用的流程是:
- 打开BLE调试助手,扫描设备,找到E104-BT02的广播名称。
- 点击设备发起连接,此时不要急着收发数据。
- 如果模块配置了安全连接要求,App会弹出配对框,输入默认PIN码(模块手册里会给,一般是6位数字)。
- 配对成功后,设备详情里会显示已绑定(Bonded)状态,部分App还会显示绑定键类型。
- 断开重连一次,确认不需要重复输入PIN码,说明绑定已经生效。
绑定时最容易出的问题是不清楚模块默认PIN码。很多透传模块出厂时PIN码是固定的,比如“000000”或“123456”,第一次配对前一定要先查规格书。如果输错太多次,模块会进入短暂的锁定状态,等几分钟再试,或者用AT指令恢复出厂设置。
4.2 GATT服务和特征值的关系
手机上能看到一个BLE设备下的服务和服务里的特征值,这些统称为GATT结构。E104-BT02这类透传模块一般会提供一个自定义的UART服务,里面包含写特征(用于手机向模块发数据)和通知特征(用于手机订阅模块发来的数据)。
调试助手操作时,打开设备详情,会看到“Unknown Service”或类似FFE0这样的UUID。点进去之后,关键是打开Notify(通知)开关。只有打开Notify,手机才能实时收到模块串口发来的数据。如果连接上了但收不到任何数据,第一件事就是检查Notify开关有没有开。
写数据时,要看清楚特征值是否支持Write属性。在BLE里,支持Write的特征值可以直接写入数据,支持Write Without Response的则不用等待对端确认。透传模块通常两个都支持,调试助手一般默认用普通写入,速度会略慢但更可靠。
4.3 MTU大小决定一包能传多少
MTU(Maximum Transmission Unit)是ATT层的最大传输单元,默认值是23字节,减去ATT协议头3个字节,实际一包只能传20字节用户数据。这意味着如果你一次往串口发100个字节,透传模块需要自己分包,而接收端App或MCU需要把这些分片重新拼起来。
把MTU调大能明显改善大包传输效率。现代智能手机基本都支持MTU协商到247,协商成功后一包最多能传244字节用户数据。BLE调试助手App里一般都有“请求MTU”或“MTU设置”的入口,手动请求247即可。
主控侧要注意的是:如果你没有做MTU协商,模块仍然按照20字节一包来分包,这个状态下应用层如果要发送超过20字节的数据,就必须自己做好重组逻辑。我们开发时遇到手机App偶发丢消息,排查下来就是MTU没有协商成功,导致一个50字节的帧被拆成3包,中途一包丢失整帧就报废。后来把MTU固定为247并加了帧校验,问题才消失。
4.4 从广播到连接成功的完整时序
理解BLE的连接时序,排查问题会快很多。一次典型的连接过程是:
- 模块上电,进入广播状态,周期性发送广播包。
- 手机扫描到广播包,用户点击连接。
- 手机向模块发送连接请求,链路层进入连接状态。
- 连接建立后,手机开始GATT服务发现,读取模块的服务列表。
- 手机打开需要使用的通知特征,订阅模块的数据。
- 如果安全策略要求,这时执行配对绑定流程。
- 绑定完成,两端进入数据通信阶段。
这套时序里任何一个环节出问题,表现都不一样。广播没开,手机搜不到;连接请求发出去但要绑定的设备可能没有完成彻底重连;服务发现被阻塞时数据迟迟不出现;通知没打开时看不到数据。我最常用的排查顺序是:先确认广播,再确认连接,再确认服务,最后确认Notify。
如果手里有ESP32开发板,也可以把它配成BLE Central(主机)去连接E104-BT02,不需要手机参与。我在一个自动化测试脚本里就是用ESP32当Central,跑了一整天连接、断开、重连的循环,用来验证模块的稳定性。注意选择与手机调试一致的GATT操作方式,不要在一个工程里混用两套API。
5. 常见问题、排查方法和工程经验
5.1 高频问题速查表
| 现象 | 常见原因 | 排查方向 |
|---|---|---|
| 手机搜不到BLE设备 | 模块未进入广播状态、广播类型不对 | 检查SET/CFG引脚电平,确认模块供电正常 |
| 能搜到但连接不上 | 模块已被另一个设备占用、绑定残留 | 断开其他连接,重启模块,必要时恢复出厂 |
| 连接后收不到数据 | Notify通知未开启 | 在调试助手里打开特征值通知 |
| 单次发送数据超过20字节丢包 | 未协商MTU或MTU协商失败 | 请求更大MTU(如247) |
| 配对总是失败 | PIN码错误或模块已锁定 | 查规格书默认PIN码,等待锁定时间结束 |
| 收发数据偶尔乱序或粘包 | 应用层没有做帧协议 | 增加帧头、长度、CRC32校验 |
| 距离短、穿墙差 | 天线区域被遮挡、电源纹波大 | 清空天线净空,独立供电并加大退耦电容 |
这些问题里,最常见也最容易被忽略的还是电源。BLE模块在广播和连接瞬间电流尖峰比较大,电源容量不足会直接表现为“单独调试正常、带上其他外设就异常”。我建议第一版PCB就按最高功耗估算电源余量,别用LDO去扛大动态负载。
5.2 几个值得记住的工程建议
关于绑定状态,我想多说一句。如果代码里要判断手机是否已绑定,可以通过模块的状态指示脚实现,但实际产品上更推荐让MCU去询问连接状态,而不是猜。绑定信息存储在模块内部,不是每次BLE连接都会重新配对,所以首次配好后,后续连接速度会快很多。但如果模块粘到了“错误绑定”的状态,最快解决办法是用串口AT指令执行恢复出厂,把绑定表清掉。
模块和主控之间一定要严格匹配电平。E104-BT02工作电压1.8~3.6V,如果你的主控是5V供电,串口TX输出5V高压,长期使用有损坏模块IO的风险。这个时候建议用两颗MOS管做电平转换,或者选支持5V容忍的GPIO,不要直接连。
调试串口的波特率也会埋雷。模块出厂默认可能是115200,但有时候二手模块或库存模块被人改过波特率,这时候可以通过AT指令重新配置。实在确认不了,就把模块恢复出厂设置,再按手册重新配置一遍参数。
最后一个经验:软件开发时一定要把串口日志和BLE透传通道分开。E104-BT02只负责业务数据透传,不要把调试日志也往里面灌,否则日志一多,业务数据容易被冲掉。我在主控上保留一个独立的调试串口,所有printf走调试串口,BLE只跑业务帧。这个习惯帮我省了大量排查时间。
实际用E104-BT02做了两个量产项目之后,我的体会是:BLE本身并没有想象中那么难,难的是把它和现有业务系统稳定地接在一起。用透传模块能把无线这层复杂度隔离掉,但应用层的帧格式、缓冲区管理、MTU协商这些基本功还是省不掉。如果你也想快速给设备加蓝牙,照着上面的电路和代码做一版,再用调试助手走一遍绑定和透传流程,大概率一个下午就能跑通。