这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【EEPW-Let'sdesign】STM32WBA62CIU6+BLE开发

共1条 1/1 1 跳转至页

【EEPW-Let'sdesign】STM32WBA62CIU6+BLE开发

助工
2026-10-19 19:04:25     打赏

很多刚接触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的内存是这样分的:

image.png

注意:协议栈固件是单独编译好的,要单独烧录到芯片里。第一次烧录的时候,要先烧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的参考例程来写,一般不会有什么幺蛾子。



共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]