1. 项目缘起与整体方案拆解
1.1 为什么选 RA4M2 搭配 DA14531 这套组合
手头这个项目需求很明确:一块主控要采集传感器数据,同时把数据无线发到手机 App 上,手机端还要能反向下发控制指令。选型阶段我对比过几种常见路子,最后定下瑞萨 RA4M2做主控、Dialog DA14531做蓝牙透传的方案,这里说说背后的取舍逻辑。
RA4M2 属于 RA4 系列,Cortex-M33 内核,主频 100MHz,带 TrustZone,片上 UART、SPI、I2C 外设齐全,最关键的是它的 SCI(串行通信接口)通道多、FIFO 深度够,跑高波特率不容易丢字节。相比一些老牌 M3/M4 芯片,RA4M2 的 FSP(Flexible Software Package)配置工具把底层寄存器封装得比较干净,省掉大量查手册的时间。而 DA14531 这颗 BLE SoC 体积小、功耗低,官方 SDK 里直接给了完整的 GATT 透传例程,改起来门槛不高。
提示:DA14531 分两个常见封装,WLCSP 那种超小封装手工焊接难度大,打样阶段建议先用模块(比如带天线的成品模组),量产再考虑裸片。
这套组合解决的核心问题是:主控负责业务逻辑和数据处理,蓝牙芯片专职无线链路,两者通过 UART 解耦。这样做的好处是蓝牙协议栈的复杂度被隔离在 DA14531 内部,主控固件不需要关心 BLE 的广播、连接、配对细节,只要按约定格式收发串口数据即可。对团队分工也友好,做业务的和调无线的可以并行推进。
1.2 整体数据链路是怎么走的
先把整条链路画清楚,后面每一步操作都围绕它展开。数据从传感器进 RA4M2,经过打包,通过 UART 发给 DA14531,DA14531 把数据塞进 GATT 特征值,手机 App 订阅后收到通知;反向则是手机写特征值,DA14531 从 UART 吐出来,RA4M2 解析后执行动作。
| 环节 | 发送方 | 接收方 | 物理接口 | 数据形态 |
|---|---|---|---|---|
| 采集 | 传感器 | RA4M2 | I2C/SPI/ADC | 原始寄存器值 |
| 打包 | RA4M2 | DA14531 | UART | 自定义帧 |
| 无线 | DA14531 | 手机 | BLE GATT | Notify/Write |
| 回控 | 手机 | DA14531 | BLE GATT | Write |
| 下发 | DA14531 | RA4M2 | UART | 自定义帧 |
这里有个容易被忽略的点:UART 是异步全双工,但 BLE 的 Notify 是单向推送。也就是说,如果手机端同时高频写、DA14531 同时高频 Notify,UART 两个方向的数据会在 DA14531 内部竞争缓冲。所以帧格式里必须带方向标识和长度字段,否则解析会错位。我在第一版就吃过这个亏,后面会详细讲。
1.3 方案的优势与要规避的坑
选这套方案,优势集中在三点。第一是开发速度快,DA14531 的透传例程改改 UUID 和波特率就能跑通,不用从零写 GATT 服务。第二是功耗可控,DA14531 在连接间隔 100ms 的情况下平均电流能压到几百微安级别,适合电池供电场景。第三是主控资源占用低,RA4M2 的 UART 用中断加环形缓冲,CPU 几乎不参与搬运。
要规避的坑也很明确。DA14531 的 UART 默认波特率上限和它的时钟配置强相关,如果主控这边设了 921600 而蓝牙侧没同步改,收到的就是乱码。另外 DA14531 的 SDK 里 UART 收发和 BLE 事件在同一个任务上下文里跑,长时间阻塞式发送会拖垮 BLE 连接稳定性,必须用非阻塞或 DMA 方式。这些细节在后面实操章节会逐个展开。
2. 核心细节解析与实操要点
2.1 RA4M2 侧 UART 驱动的关键配置
RA4M2 的 SCI 外设配置有几个参数直接决定通信是否可靠,我按重要性排一下。首先是波特率发生器的时钟源,FSP 里默认用 PCLK,如果系统时钟改了而没重算分频,实际波特率会偏。计算公式是:
BRR = PCLK / (波特率 × 分频系数) - 1以 PCLK = 48MHz、目标波特率 115200、分频系数 16 为例,BRR = 48000000 / (115200 × 16) - 1 ≈ 25.04,取整 25,实际波特率约 115384,误差 0.16%,在容忍范围内。但如果目标波特率是 921600,误差会放大,这时候建议把分频系数调小或换更高 PCLK。
其次是FIFO 触发深度。RA4M2 的 SCI 收发 FIFO 各 16 字节,接收触发点我一般设成 8 字节,这样中断频率和缓冲效率比较平衡。设成 1 字节会导致中断风暴,设成 16 字节又容易在最后一帧不满时延迟处理。
第三是错误中断使能。溢出错误(ORE)、帧错误(FE)、奇偶校验错误(PE)都要开中断,否则一旦出错标志不清,后续数据全废。我在调试时遇到过一次 ORE 没清导致接收卡死,排查了半天才发现是错误中断没处理。
/* RA4M2 SCI 接收回调示例,基于 FSP */ void uart_callback(uart_callback_args_t *p_args) { switch (p_args->event) { case UART_EVENT_RX_CHAR: ring_buffer_push(&rx_buf, (uint8_t)p_args->data); break; case UART_EVENT_ERR_OVERFLOW: case UART_EVENT_ERR_FRAMING: case UART_EVENT_ERR_PARITY: uart_err_flag = 1; /* 关键:清错误标志,否则接收停摆 */ R_SCI_UART_StatusClear(&g_uart_ctrl); break; default: break; } }注意:FSP 生成的代码里,错误事件回调如果不手动清标志,某些型号的 SCI 会一直停在错误状态。这个坑在官方文档里写得比较隐晦,实测必须加。
2.2 DA14531 侧 GATT 透传服务搭建
DA14531 的 SDK 里有个ble_app_peripheral例程,本身就是做透传的,我们主要改三处:服务 UUID、特征值属性、UART 波特率。默认例程用的是 Dialog 自己的 UUID,量产项目建议换成自定义 128 位 UUID,避免和别的设备撞车。
特征值属性这块要重点说。透传需要两个特征值:一个用于手机接收数据(带 Notify 属性),一个用于手机发送数据(带 Write 或 Write Without Response 属性)。如果手机端写入频率高,建议用Write Without Response,省掉每包确认的开销,吞吐能提升不少。但代价是没有重传保障,丢包要靠上层协议补。
| 特征值 | 属性 | 用途 | 建议 |
|---|---|---|---|
| Char_RX | Notify | 设备到手机 | 开 CCCD 订阅 |
| Char_TX | Write / Write NR | 手机到设备 | 高频用 Write NR |
| Char_CFG | Read/Write | 参数配置 | 加认证 |
波特率配置在user_periph_setup.h里,通过UART_BAUDRATE_115200这类宏定义。DA14531 的 UART 时钟来自内部 RC 或外部晶振,如果用的是内部 RC,波特率误差会偏大,长帧容易出错。建议在板子上留外部晶振位置,尤其是要跑 460800 以上波特率的时候。
2.3 帧格式设计:让双向透传不打架
前面提到 UART 双向和 BLE 双向会竞争,解决办法就是设计一个带方向、长度、校验的帧格式。我用的格式如下:
| 帧头(2B) | 方向(1B) | 长度(2B) | 载荷(NB) | CRC16(2B) |帧头固定0xAA 0x55,方向0x01表示主控到蓝牙,0x02表示蓝牙到主控。长度字段是载荷字节数,最大 512。CRC16 用 Modbus 多项式,覆盖方向和长度加载荷。
为什么不用简单的换行分隔?因为载荷里可能包含任意二进制数据,换行符会误判。为什么长度用 2 字节?因为 BLE 单次 Notify 最大 244 字节(MTU 247 时),加上帧头帧尾不超过 250,2 字节足够且留了扩展余量。
提示:CRC 校验一定要覆盖长度字段,否则长度被干扰后解析会越界。我见过有人只校验载荷,结果长度字段出错导致缓冲区溢出。
2.4 手机端连接与 MTU 协商
手机端不管是 Android 还是 iOS,连接后第一件事是请求 MTU 协商。默认 MTU 是 23,有效载荷只有 20 字节,透传效率极低。Android 上调用requestMtu(247),iOS 会自动协商到 185 左右。MTU 越大,单包能带的数据越多,但也不是越大越好,超过 247 后部分手机兼容性会下降。
连接间隔(Connection Interval)也要调。默认可能是 30ms 或 50ms,如果数据量大,可以请求更小的间隔,比如 15ms,但会增加功耗。反过来如果只是偶尔发数据,间隔设大点省电。这个参数由手机端发起请求,设备端可以拒绝或接受。
3. 实操过程与核心环节实现
3.1 硬件连接与上电检查
先把硬件接起来。RA4M2 的 SCI 通道我选的是 SCI0,对应引脚 P410(TX)和 P411(RX),交叉接到 DA14531 模块的 RX 和 TX。注意一定要交叉,TX 接 RX,RX 接 TX,这个低级错误我见过不止一次。共地必须接,否则电平参考不一致,通信时好时坏。
上电顺序也有讲究。先给 RA4M2 上电,再给 DA14531 上电,因为 DA14531 启动时会拉高 UART TX 一段时间,如果主控还没初始化好,可能收到垃圾数据。稳妥做法是主控初始化完 UART 后再给蓝牙模块复位。
检查清单:
- 用万用表量 TX/RX 对地电压,空闲时应为高电平(3.3V)
- 示波器看 TX 是否有波形,没有则检查引脚复用配置
- 确认两边电平一致,DA14531 是 3.3V 逻辑,RA4M2 也配 3.3V
3.2 RA4M2 固件:从初始化到收发闭环
FSP 里新建工程,添加 UART 栈,配置好波特率、数据位、停止位、校验位。数据位 8、停止位 1、无校验是标配。然后写收发逻辑。
发送部分我封装了一个函数,把载荷打包成帧再发:
void send_frame(uint8_t dir, const uint8_t *payload, uint16_t len) { uint8_t frame[520]; uint16_t idx = 0; frame[idx++] = 0xAA; frame[idx++] = 0x55; frame[idx++] = dir; frame[idx++] = (len >> 8) & 0xFF; frame[idx++] = len & 0xFF; memcpy(&frame[idx], payload, len); idx += len; uint16_t crc = crc16_modbus(&frame[2], idx - 2); frame[idx++] = (crc >> 8) & 0xFF; frame[idx++] = crc & 0xFF; /* 非阻塞发送,避免拖住主循环 */ R_SCI_UART_Write(&g_uart_ctrl, frame, idx); }接收部分用状态机解析,逐字节喂入,遇到帧头开始计数,收满一帧后校验 CRC,通过则交给业务层。状态机的好处是不会因为半包数据卡死,超时后自动复位。
typedef enum { WAIT_H1, WAIT_H2, WAIT_DIR, WAIT_LEN_H, WAIT_LEN_L, WAIT_PAYLOAD, WAIT_CRC_H, WAIT_CRC_L } parse_state_t; void parse_byte(uint8_t b) { static parse_state_t st = WAIT_H1; static uint8_t buf[520]; static uint16_t pos, need; switch (st) { case WAIT_H1: if (b == 0xAA) st = WAIT_H2; break; case WAIT_H2: st = (b == 0x55) ? WAIT_DIR : WAIT_H1; break; case WAIT_DIR: buf[0] = b; st = WAIT_LEN_H; break; case WAIT_LEN_H: need = b << 8; st = WAIT_LEN_L; break; case WAIT_LEN_L: need |= b; pos = 0; st = need ? WAIT_PAYLOAD : WAIT_CRC_H; break; case WAIT_PAYLOAD: buf[1 + pos++] = b; if (pos >= need) st = WAIT_CRC_H; break; case WAIT_CRC_H: /* 存 CRC 高字节 */ st = WAIT_CRC_L; break; case WAIT_CRC_L: /* 校验并分发 */ st = WAIT_H1; break; } }3.3 DA14531 固件:透传逻辑与 UART 对接
DA14531 这边,SDK 的user_send_ble_data和user_uart_callback是两个核心入口。UART 收到数据后,不要直接在大回调里调ble_send,而是丢进队列,由 BLE 任务空闲时取出发送。原因是 BLE 发送依赖连接状态和缓冲,在中断上下文里调用可能失败。
/* UART 回调里只入队 */ void user_uart_callback(uint8_t status) { while (uart_rx_available()) { uint8_t b = uart_rx_get(); ring_push(&ble_tx_ring, b); } /* 触发主循环处理 */ ble_tx_pending = 1; } /* 主循环里出队发送 */ void ble_tx_process(void) { if (!ble_tx_pending || !conn_active) return; uint8_t chunk[244]; uint16_t n = ring_pop_batch(&ble_tx_ring, chunk, sizeof(chunk)); if (n) { user_send_ble_data(chunk, n); } ble_tx_pending = ring_count(&ble_tx_ring) > 0; }波特率两边必须一致,我统一用 115200 起步,跑通后再往上调。DA14531 的 UART 配置在user_periph_setup.c里,改uart_cfg结构体的波特率字段即可。
3.4 手机端联调与数据验证
手机端我用的是通用的 BLE 调试 App,先扫描,找到设备名,连接,然后找到透传服务,订阅 Notify 特征值,往 Write 特征值写数据。验证顺序建议是:
- 先测单向:主控发固定字符串,手机看是否收到
- 再测反向:手机写数据,主控串口打印看是否收到
- 最后测双向并发:两边同时高频收发,看是否丢包或错位
我实测下来,115200 波特率、MTU 247、连接间隔 30ms 的情况下,单向吞吐能到 20KB/s 左右,双向并发时降到 12KB/s 上下,这个数据对大多数传感器采集场景够用了。
4. 常见问题与排查技巧实录
4.1 通信类问题速查表
调试过程中遇到的问题我整理成表,方便对照排查。
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 收到乱码 | 波特率不一致 | 示波器测位宽 | 两边统一波特率 |
| 完全无数据 | TX/RX 未交叉 | 量引脚波形 | 交叉接线 |
| 偶发丢包 | FIFO 溢出 | 看 ORE 标志 | 加环形缓冲、提中断优先级 |
| 连接后断开 | 连接参数不兼容 | 抓 BLE 日志 | 调整连接间隔 |
| Notify 收不到 | CCCD 未订阅 | 检查描述符写 | 手机端订阅 |
| 长帧出错 | 缓冲不足 | 看帧长 | 增大缓冲或分片 |
4.2 几个我踩过的坑
第一个坑是 DA14531 的 UART 唤醒延迟。DA14531 在睡眠模式下,UART 收到第一个字节时会有唤醒时间,如果主控发得太快,第一个字节可能丢。解决办法是在帧头发送前先发一个 dummy 字节,或者让蓝牙侧保持 UART 常开。我选择的是后者,功耗略高但稳定。
第二个坑是 CRC 计算范围。一开始我把帧头也算进 CRC,结果和手机端对不上。后来统一约定 CRC 从方向字段开始算,问题解决。这种协议细节一定要在文档里写死,不然两边实现容易分歧。
第三个坑是 BLE 写操作的流控。手机端连续 Write Without Response 时,如果 DA14531 处理不过来,会静默丢包。SDK 里有流控机制,但需要正确配置缓冲数量。我把 BLE 发送缓冲从默认的 4 个加到 8 个,丢包率明显下降。
注意:DA14531 的 RAM 有限,缓冲不是越多越好,加太多会导致其他功能内存不足。建议根据实际吞吐需求调,别盲目拉满。
4.3 性能调优的几个方向
如果基础功能跑通了想再压榨性能,可以从这几个方向入手。提高波特率到 460800 或 921600,前提是两边时钟精度够。增大 MTU到 247,减少协议头开销。缩短连接间隔到 15ms,降低延迟但增加功耗。启用 DLE(数据长度扩展),让单包能带更多数据。
这几个参数是相互影响的,比如 MTU 大了但连接间隔长,实际吞吐未必提升。我的建议是先用默认参数跑通,再逐个调整,每次只改一个变量,记录吞吐和稳定性数据,找到适合自己场景的平衡点。
5. 项目收尾与后续扩展思路
整套东西跑通之后,我最大的体会是协议设计比代码实现更重要。帧格式、CRC 范围、流控策略这些定好了,后面换主控、换蓝牙芯片都能复用,只是驱动层重写。反过来如果协议没设计好,代码写得再漂亮,联调时也是无尽的扯皮。
后续如果要扩展,我打算往两个方向走。一是加 OTA 升级,通过 BLE 给 RA4M2 传固件,这样设备装到现场后不用拆机就能更新。二是多连接支持,让一个 DA14531 同时连手机和另一个从设备,做数据中继。这两个方向都需要在现有帧格式上加命令类型字段,算是平滑演进。
最后分享一个小技巧:调试 BLE 时,如果手机端抓不到包,可以先用串口把 DA14531 的日志打出来,看它到底有没有收到连接请求、有没有成功订阅。很多时候问题不在无线,而在设备端的 GATT 配置。把日志打通,排查效率能翻好几倍。