很多刚接触STM32WB/WBA系列的朋友都会有个疑问:为什么这个芯片要分两个核?BLE协议栈到底是怎么跑起来的?写代码的时候云里雾里,出了问题都不知道去哪查。
一、为什么BLE芯片要搞两个核?
你可能会问:不就是个BLE嘛,一个核跑应用,一个核跑协议栈,为什么不能合在一起?
原因是BLE协议栈对实时性要求非常高。蓝牙规定了精确的时隙,广播、连接事件都要在准确的时间点发生,差几微秒都可能导致连接超时、丢包。
如果协议栈和应用跑在同一个核上,万一应用代码卡在某个延时、或者被高优先级中断打断,协议栈的实时性就没保证了——蓝牙断连了都不知道为什么。
所以ST的做法是:
• 一个专门的核(Cortex-M0+)跑BLE协议栈,实时性有保证;
• 另一个核(Cortex-M33)跑用户的应用代码,爱怎么写怎么写;
• 两个核之间通过共享内存+中断的方式通信。
这样设计的好处是:应用代码再怎么烂,也不会把蓝牙搞断。这就是为什么很多BLE芯片都用双核架构——nRF52是M4+M0,Silicon Labs的也是,都是这个思路。
二、STM32WBA62的双核架构详解
2.1 两个核心分别是什么?
STM32WBA62里面有两个CPU核心:
• **Cortex-M33**:主核,跑100MHz,用户应用代码都跑在这个核上。我们写的传感器采集、业务逻辑、UI控制,全在M33上;
• **Cortex-M0+**:无线核(也叫WRAP),跑48MHz,专门跑BLE协议栈和802.15.4协议栈。这个核的代码是ST做好的固件,用户一般不用改。
两个核共享同一块Flash和RAM,但是各用各的部分——Flash的一部分给协议栈,一部分给应用;RAM也是分成两块,中间留一块做通信缓冲区。
2.2 两个核是怎么通信的?
两个核之间用的是IPCC(Inter-Processor Communication Controller,核间通信控制器)。简单说就是:
1. M33要发命令给协议栈 → 把参数放到共享内存的约定位置,然后触发一个IPCC中断告诉M0+:"嘿,有命令了";
2. M0+收到中断 → 去共享内存读命令,执行,然后把结果写回共享内存,再触发IPCC中断告诉M33:"搞定了,来拿结果";
3. 反过来也一样,协议栈要通知应用(比如收到了手机的数据、连接状态变化了),也是通过IPCC中断告诉M33。
这个机制其实就是个邮箱系统,两边通过邮箱收发消息。ST已经把这个封装好了,我们用的时候不用直接操作IPCC,调用BLE库的API就行。
2.3 内存是怎么分配的?
这个很重要,很多新手会搞混。STM32WBA的内存是这样分的:

注意:协议栈固件是单独编译好的,要单独烧录到芯片里。第一次烧录的时候,要先烧M0+的协议栈固件,再烧M33的应用程序。ST的STM32CubeProgrammer会自动处理这个,你不用手动分地址烧。
三、BLE协议栈的层次结构
BLE协议栈不是一坨代码,是分层的。从下到上大概是这样:
3.1 PHY层(物理层)
最底层,负责2.4GHz射频信号的收发。这部分完全在硬件里,用户接触不到。我们调的发射功率、接收灵敏度,就是这一层的参数。
3.2 Link Layer(链路层)
负责广播、扫描、连接、数据收发这些底层逻辑。什么时候发广播包、连接事件什么时候开始,都是这一层管的。这部分也是ST做好的,用户不用管。
3.3 Host层(主机层)
这一层是我们接触最多的,包括GAP、GATT这些协议。
• GAP(通用访问配置文件):负责设备怎么被发现、怎么连接。就是广播参数、设备名称、连接参数这些东西。
• GATT(通用属性配置文件):定义了服务、特征、描述符这些概念,是我们定义数据格式的地方。比如我们做一个温湿度服务,里面有温度特征、湿度特征,就是在GATT层定义的。
• ATT(属性协议):GATT的底层,负责读写属性的具体流程。
3.4 Profile层(应用层)
就是我们自己写的应用代码了。比如温湿度传感器、心率带、蓝牙键盘这些具体的应用,都在这一层。我们调用GATT的API,定义自己的服务和特征,处理数据收发。
四、一次BLE通信的完整流程
1. **设备上电**:M33核跑应用代码,初始化之后,调用BLE库的API开始广播;
2. **协议栈发广播**:M0+核的链路层每隔一定时间,在3个广播信道上发广播包,里面带设备名称、服务UUID这些信息;
3. **手机扫描**:手机BLE扫描到广播包,看到有个叫"WBA_TempSensor"的设备;
4. **手机发起连接**:手机发连接请求,链路层收到,建立连接;
5. **通知应用**:协议栈通过IPCC告诉M33应用:"连上了!";
6. **手机发现服务**:手机发GATT读请求,读我们的服务列表,找到温湿度服务;
7. **手机读温度**:手机发GATT读请求,读温度特征的值;
8. **应用处理请求**:协议栈收到读请求,通过IPCC通知M33:"有人读温度了";
9. **应用返回数据**:M33去读传感器,拿到温度值,通过BLE库API把数据返回给协议栈;
10. **协议栈发回手机**:M0+核把温度数据通过连接事件发给手机,完成一次通信。
整个过程看起来步骤很多,但是ST已经把底层都封装好了,我们应用代码里只需要处理第8步和第9步——收到读请求的时候,把数据填进去就行。
五、写BLE应用代码的几个注意点
1. **主循环里必须跑协议栈任务**:STM32WBA的主循环里必须不断调用MX_APPE_Process(),不然协议栈跑不起来。这个和普通STM32不一样,普通STM32你在主循环里写自己的逻辑就行,BLE的话要给协议栈留时间。
2. **不要在中断里做耗时操作**:BLE协议栈对实时性要求高,如果你的中断服务函数太长,把协议栈的任务打断了,可能会导致连接不稳定。中断里只做标记,具体处理放到主循环里做。
3. **BLE库的API要按顺序调用**:比如你不能还没初始化就开始广播,不能没连接就发数据。每个API调用都有状态要求,调用不对会返回错误码。
4. **所有BLE事件都是回调通知的**:比如连接成功、收到数据、断开连接,这些都是通过回调函数通知你的,你要在回调函数里处理对应的事件。不要自己轮询状态。
六、GATT的三个基本概念
写GATT服务之前,先搞懂三个基本概念:服务(Service)、特征(Characteristic)、描述符(Descriptor)。
6.1 服务(Service)
服务是一组相关功能的集合。比如我们做温湿度传感器,就可以定义一个"温湿度服务",里面包含温度、湿度两个特征。每个服务有个唯一的UUID。
6.2 特征(Characteristic)
特征是服务里面的具体数据项。每个特征也有UUID,还有属性——可读、可写、可通知(Notify)。比如温度特征就是只读的,手机可以读它的值;LED控制特征就是可写的,手机可以写值来控制LED。
6.3 描述符(Descriptor)
描述符是对特征的补充说明。最常用的是CCC描述符(Client Characteristic Configuration),用来开/关Notify功能——手机要订阅Notify,就是写这个描述符。
我们做串口透传,就定义一个透传服务,里面两个特征:
• **RX特征**(手机→设备):手机写数据到这个特征,设备收到后从串口发出去;
• **TX特征**(设备→手机):设备通过Notify发数据到这个特征,手机收到后显示出来。
七、用CubeMX配置GATT服务
不用手写GATT表,STM32CubeMX里有图形化配置界面,点点鼠标就行。
7.1 打开BLE配置界面
在CubeMX的中间件里选STM32_Wireless,然后点Customization,就能看到GATT配置界面了。
7.2 添加自定义服务
点"Add Service",给自己的服务起个UUID。UUID可以随便选,比如用128位的自定义UUID,就不会和标准服务冲突。
我们用个简单的:0000fff0-0000-1000-8000-00805f9b34fb,这是个常用的透传服务UUID。
7.3 添加两个特征
在这个服务下面加两个特征:
① **RX特征**(UUID: 0000fff1):属性设为Write + Write Without Response,最大长度设为20字节(BLE默认MTU是23,去掉包头就是20);
② **TX特征**(UUID: 0000fff2):属性设为Notify,最大长度也设为20字节。
配置完了之后,点生成代码,CubeMX会自动把GATT表都生成好,不用我们手写。
八、处理写请求:手机发数据给设备
当手机往RX特征写数据的时候,协议栈会触发一个回调事件。我们要在这个回调里把数据读出来,然后从串口发出去。
这个回调函数叫Custom_APP_GATT_WriteRequestEvent,在app_gatt.c文件里。大概是这样:
void Custom_APP_GATT_WriteRequestEvent(uint16_t ConnectionHandle,
uint16_t AttributeHandle,
uint16_t DataLength,
uint8_t *PayloadData)
{
// 看看是哪个特征被写了
if (AttributeHandle == RX_CHAR_HANDLE)
{
// 把收到的数据从串口发出去
for (int i = 0; i < DataLength; i++)
{
HAL_UART_Transmit(&huart1, &PayloadData[i], 1, 100);
}
}
}
这里要注意:PayloadData指针指向的数据只在这个回调函数里有效,回调返回之后就可能被覆盖了。如果你要把数据存起来慢慢处理,一定要自己copy一份到缓冲区里。
九、发Notify:设备把串口数据发给手机
反过来,串口收到数据之后,我们要通过TX特征的Notify把数据发给手机。
首先要确认手机已经订阅了Notify——就是手机写了CCC描述符,开了Notify。这个也是在GATT事件回调里处理的:
// 记录Notify是否已经开启
uint8_t notify_enabled = 0;
void Custom_APP_GATT_WriteRequestEvent(uint16_t ConnectionHandle,
uint16_t AttributeHandle,
uint16_t DataLength,
uint8_t *PayloadData)
{
// 如果是写CCC描述符,说明手机要开/关Notify
if (AttributeHandle == TX_CHAR_CCCD_HANDLE)
{
if (PayloadData[0] == 0x01)
{
notify_enabled = 1; // Notify开了
}
else
{
notify_enabled = 0; // Notify关了
}
}
}
然后,串口收到数据之后,调用BLE的Notify发送函数:
// 串口收到一个字节,放到缓冲区
uint8_t uart_rx_buf[64];
uint8_t uart_rx_len = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == &huart1)
{
if (uart_rx_len < 64)
{
uart_rx_buf[uart_rx_len++] = rx_byte;
}
// 继续接收下一个字节
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
// 主循环里检查,缓冲区有数据就发Notify
void MX_APPE_Process(void)
{
if (notify_enabled && uart_rx_len > 0)
{
// 调用BLE库的Notify发送函数
tBleStatus status = aci_gatt_notification(
conn_handle,
TX_CHAR_VALUE_HANDLE,
uart_rx_len,
uart_rx_buf
);
if (status == BLE_STATUS_SUCCESS)
{
uart_rx_len = 0; // 发成功了,清空缓冲区
}
}
}
这里有几个注意点:
1. 一次Notify最多发20字节(默认MTU),超过的话要分包发;
2. 发送失败的时候不要马上重试,等下一个连接事件再发,不然会占CPU;
3. 缓冲区要做环形队列,不然串口收得快,BLE发得慢,会丢数据。
十、用手机APP测试
代码写完编译下载,打开手机的nRF Connect APP:
1. 扫描到我们的设备,点连接;
2. 找到我们的透传服务(fff0那个);
3. 点TX特征下面的下拉箭头,订阅Notify;
4. 在RX特征那里点写按钮,发几个字节;
5. 看看串口助手有没有收到数据;
6. 反过来,串口助手发几个字节,看看手机APP的Notify有没有收到。
如果双向都能收到数据,BLE透传就做通了!
十一、常见问题排查
11.1 问题一:手机扫描不到设备
这是最常见的问题,刚上电,手机APP搜半天搜不到。按下面的顺序查:
① 先确认广播有没有开——代码里有没有调用开始广播的函数?有没有错误返回?可以在代码里打个日志,广播启动成功了就打一行"Advertising started"。如果启动广播就报错,那是协议栈状态不对。
② 查广播参数对不对——广播类型是不是设成了可连接的?广播间隔是不是太大了?一般设100ms~1秒就行。
③ 查硬件射频部分——32MHz晶振起振了没有?天线有没有焊好?发射功率是不是设成0了?
④ 换个手机试试——有时候不是板子的问题,是手机的问题。
11.2 问题二:刚连上就断连
能搜到,点连接,连上两秒钟就断了。
① 查连接参数——连接间隔设得太小了?超时时间设得太短?建议设保守一点:连接间隔100ms~500ms,超时时间2~4秒。
② 查软件有没有处理连接事件——连接成功之后,协议栈要给主机发事件,如果应用层没及时处理,协议栈会认为主机有问题,就把连接断了。主循环里一定要及时跑协议栈任务。
③ 查射频信号是不是太弱——用手机APP看RSSI值,如果低于-85dBm,那大概率是射频不行,要调天线匹配。
11.3 问题三:传输丢包
传数据的时候有时候收不到。
① 一次发太多——默认BLE MTU是23字节,一次最多发20字节有效数据。超过的话要分包发,或者先协商提高MTU。
② 发送太快——BLE是按连接间隔收发的,比如连接间隔100ms,那每100ms才能发一个包。一口气发一堆,协议栈缓冲区满了就丢包。正确做法是发一个包,等发送完成的事件回调,再发下一个。
③ 信号干扰——2.4GHz频段干扰很多,WiFi、微波炉都可能干扰。换个广播信道试试,或者用BLE 5.0的编码PHY。
11.4 问题四:通信距离太近
BLE理论上能传几十米,实际用起来只有两三米。
① 先确认发射功率是不是最大——先设成+10dBm试试。
② 天线匹配没调好——PCB天线的尺寸、匹配网络的参数,直接抄参考设计不一定能工作在最佳状态。调整匹配网络的电容电感,看哪个参数下RSSI最大。
③ 天线净空不够——PCB天线下面不能有铜皮,周围不能有金属。检查一下天线区域的PCB是不是全挖空了。
十二、排查BLE问题的通用思路
遇到BLE问题不要瞎猜,按这个顺序查:
**第一步:先看软件**——代码里API调用对不对?有没有错误返回?事件回调有没有处理?
**第二步:再看参数**——广播参数、连接参数、发射功率,这些是不是设合理了?
**第三步:最后看硬件**——晶振起振没有?天线焊好没有?射频匹配有没有问题?
很多人一上来就查硬件,其实80%的问题都是软件和参数的问题。
另外,善用手机APP:nRF Connect能看RSSI、能看GATT表、能发各种命令,是调试BLE最好用的工具,比接示波器方便多了。
十三、总结
STM32WBA的双核架构其实不难理解:M0+专门跑协议栈,保证实时性;M33跑用户应用,开发自由度高。两个核通过IPCC通信,ST把这些都封装好了,我们用的时候只需要调用BLE库的API就行。
做BLE透传其实不难,核心就是两个特征:一个写(手机→设备),一个Notify(设备→手机)。理解了GATT的服务、特征、属性这些概念,其他BLE应用都是类似的——无非就是换个数据内容。
BLE开发就是这样,写代码一天,调bug三天。但是常见的问题就那么几个,踩过一次坑,下次就知道怎么查了。STM32WBA的协议栈还是很稳定的,只要硬件射频没大问题,软件按ST的参考例程来写,一般不会有什么幺蛾子。
我要赚赏金
