1. 项目缘起与整体方案拆解
1.1 为什么选 RA4M2 搭配 DA14531 这套组合
手头这个项目最初的诉求很朴素:一块主控板要采集几路传感器数据,然后把这些数据实时发到手机上显示,同时手机端还能下发几条控制指令回来。说白了就是双向透传,数据量不大,但对稳定性和功耗有要求。
选型的时候我对比过几种常见路子。一种是主控直接跑蓝牙协议栈,比如用带 BLE 的 SoC 一把梭;另一种是主控专心做业务逻辑,蓝牙部分交给一颗独立的 BLE 模组,两者之间用 UART 串口通信。我最后走的是第二条路,主控用RA4M2,蓝牙模组用DA14531。
这么选的理由很实在。RA4M2 是瑞萨 RA 系列里比较均衡的一颗 Cortex-M33,主频 100MHz,带 TrustZone,外设资源够用,UART、SPI、I2C 都齐全,做数据采集和逻辑控制很舒服。而 DA14531 是 Dialog(现属瑞萨)的一颗超低功耗 BLE SoC,官方叫它 SmartBond TINY,封装小、功耗低,待机电流能做到微安级,特别适合电池供电的场景。两者分工明确:RA4M2 管业务,DA14531 管无线,中间用 UART 这条最成熟、最不容易出幺蛾子的链路连接。
提示:DA14531 本身是一颗可编程的 SoC,不是那种只能发 AT 指令的透传模组。这意味着你需要往它里面烧固件,而不是简单接线就能用。这一点是很多新手第一个卡壳的地方,后面会详细讲。
1.2 整体数据链路是怎么走的
先把整条链路画清楚,后面所有细节都围绕它展开。手机 App 通过 BLE 连接 DA14531,DA14531 把收到的 BLE 数据通过 UART 转发给 RA4M2;反过来,RA4M2 把要发的数据通过 UART 交给 DA14531,DA14531 再通过 BLE 通知(Notify)推给手机。整条链路是双向的,任何一端发起都能通。
这里有个关键设计点:DA14531 在中间扮演的是"翻译官"角色,它不关心数据内容是什么,只负责把 BLE 的 GATT 读写事件和 UART 的收发字节互相搬运。这种"透传"思路的好处是业务逻辑完全解耦,RA4M2 端的代码不需要懂任何蓝牙协议,手机端也不需要懂 RA4M2 的寄存器,双方只约定一个简单的数据格式就行。
我实际用的数据格式很简单:每帧以0xAA 0x55开头,第三个字节是长度,后面是负载,最后加一个校验和。这个格式不是必须的,你也可以用纯文本加换行符,但二进制帧在解析效率和抗干扰上更稳,尤其是数据里可能混入和帧头相同的字节时,有长度字段就能准确切分。
1.3 这套方案适合谁参考
如果你正在做这几类事情,这篇内容应该能帮上忙:一是主控 + 独立 BLE 模组的双芯片架构,想搞清楚两者怎么配合;二是需要 BLE 双向通信但数据量不大的物联网设备,比如传感器节点、小型穿戴、遥控器;三是想入门 DA14531 这颗芯片,但被它那套 SDK 和工具链劝退的人。
需要说明的是,DA14531 的开发门槛确实比那些 AT 指令模组高不少。它需要你装一套专门的 SDK,用 Keil 或者官方 IDE 编译,还要通过 JTAG 或者 UART 下载固件。但换来的是极低的功耗和完全可定制的协议栈行为,这笔账怎么算取决于你的项目需求。如果只是做个玩具验证想法,用 AT 模组更快;如果是正经产品,DA14531 这种可编程方案更值得投入。
2. 硬件连接与 UART 驱动细节
2.1 引脚分配与电平匹配
RA4M2 和 DA14531 之间的 UART 连接,本质上就是交叉接线:RA4M2 的 TX 接 DA14531 的 RX,RA4M2 的 RX 接 DA14531 的 TX,两边 GND 共地。听起来简单,但实际接线时有两个坑必须提前说清楚。
第一个坑是电平。RA4M2 的 IO 电压通常是 3.3V,DA14531 也是 3.3V 供电,所以理论上可以直接连。但要注意 DA14531 的某些引脚在复位阶段或者深睡眠唤醒瞬间会有不确定状态,如果这时候 RA4M2 正在猛发数据,可能会在 DA14531 的 RX 上产生毛刺。我的做法是在 TX 线上串一个 100 欧姆的电阻,既能限流又能稍微抑制振铃,实测下来很稳。
第二个坑是流控。UART 有硬件流控(RTS/CTS)和软件流控(XON/XOFF)两种。透传场景下数据量不大,我一开始没接流控,结果在手机端快速连续下发指令时偶尔丢字节。后来加上了 RTS/CTS 硬件流控,问题就没了。如果你也遇到偶发丢数据,优先检查流控。
| 信号 | RA4M2 侧 | DA14531 侧 | 说明 |
|---|---|---|---|
| TX | P4_01 | P0_5 | 主控发,模组收 |
| RX | P4_02 | P0_4 | 主控收,模组发 |
| RTS | P4_03 | P0_7 | 主控请求发送 |
| CTS | P4_04 | P0_6 | 模组允许发送 |
| GND | GND | GND | 必须共地 |
2.2 RA4M2 端 UART 初始化要点
RA4M2 的 UART 外设叫 SCI(Serial Communications Interface),配置起来和 STM32 的 USART 思路类似,但寄存器命名和库函数不一样。我用的是瑞萨的 FSP(Flexible Software Package)配合 e2 studio,图形化配置生成代码,省去很多手动填寄存器的麻烦。
配置的时候有几个参数要盯紧。波特率我选的是115200,这个速率对透传足够,又不至于太高导致误码。数据位 8,停止位 1,无校验,这是最通用的组合。中断优先级要设得合理,UART 接收中断不能太低,否则在系统忙的时候会丢数据;但也不能太高,不然会打断其他关键任务。我一般把接收中断设成中等偏上。
/* RA4M2 UART 初始化核心参数(FSP 生成后手动确认) */ uart_cfg.baud_setting = 115200; uart_cfg.data_bits = UART_DATA_BITS_8; uart_cfg.parity = UART_PARITY_OFF; uart_cfg.stop_bits = UART_STOP_BITS_1; uart_cfg.flow_control = UART_FLOW_CONTROL_CTS_RTS;注意:FSP 生成的代码里,波特率的实际值是根据时钟源分频算出来的,不一定正好是 115200。你要在生成的
hal_data.c里确认一下实际波特率误差,一般要求误差小于 2%,否则长时间通信会累积偏差导致误码。
2.3 DA14531 端 UART 的坑
DA14531 的 UART 配置比 RA4M2 麻烦,因为它的引脚是复用的,而且 SDK 里 UART 的初始化时机很讲究。你不能在main函数一进来就初始化 UART,得等系统时钟和电源管理模块都就绪之后才行。我踩过的坑是:在periph_init里初始化 UART,结果发现波特率怎么都不对,后来查手册才知道那时候时钟还没切到正确的源。
正确的做法是在user_app_init或者app_on_init回调里做 UART 初始化。另外 DA14531 的 UART 支持 DMA,透传场景强烈建议开 DMA,否则高频收发时 CPU 会被中断拖垮,功耗也下不来。
/* DA14531 UART 初始化(在 app_on_init 中调用) */ void user_uart_init(void) { uart_cfg_t cfg = {0}; cfg.baud_rate = UART_BAUDRATE_115200; cfg.data_bits = UART_DATABITS_8; cfg.stop_bits = UART_STOPBITS_1; cfg.parity = UART_PARITY_NONE; cfg.use_dma = true; cfg.rx_dma_channel = 0; cfg.tx_dma_channel = 1; uart_initialize(&cfg, user_uart_rx_cb, user_uart_tx_cb); }这里user_uart_rx_cb是接收回调,BLE 数据往 UART 发的时候会用到;user_uart_tx_cb是发送完成回调,用来触发下一包发送。两个回调配合,就能实现不阻塞的连续收发。
3. BLE 服务与双向透传实现
3.1 自定义 GATT 服务怎么设计
BLE 通信的核心是 GATT(通用属性协议),说白了就是手机和模组之间约定好一组"特征值",手机往某个特征值写数据,模组从另一个特征值读或者接收通知。透传场景我一般设计两个特征值:一个用于手机下发(Write),一个用于模组上报(Notify)。
UUID 可以自己定义,也可以用一个常见的透传服务 UUID。我习惯自己定义,避免和系统服务冲突。服务 UUID 用 128 位自定义,两个特征值也各自给 128 位 UUID。手机端连接后先扫描服务,找到这两个特征值,然后订阅 Notify,就可以双向通信了。
| 角色 | UUID 类型 | 属性 | 用途 |
|---|---|---|---|
| 透传服务 | 128 位自定义 | Primary Service | 容器 |
| 下行特征值 | 128 位自定义 | Write Without Response | 手机发数据 |
| 上行特征值 | 128 位自定义 | Notify | 模组发数据 |
用 Write Without Response 而不是 Write With Response,是因为透传场景追求速度,不需要每条指令都等确认。如果数据重要,可以在应用层自己做 ACK,比 BLE 层的确认更灵活。
3.2 数据从 BLE 到 UART 的搬运
当手机往"下行特征值"写数据时,DA14531 的协议栈会触发一个写回调。在这个回调里,你要做的就是把收到的数据原封不动地通过 UART 发给 RA4M2。听起来简单,但有几个细节要注意。
第一,回调里不能做耗时操作。BLE 协议栈的回调是在中断上下文或者高优先级任务里跑的,你如果在里面死等 UART 发送完成,会阻塞协议栈,导致连接不稳定甚至断开。正确做法是把数据拷到一个环形缓冲区,然后触发 UART DMA 发送,发完在 DMA 完成回调里再处理下一包。
第二,数据长度要检查。BLE 单次写入的最大长度受 MTU 限制,默认 MTU 是 23 字节,实际负载 20 字节。如果你手机端一次发超过 20 字节,协议栈会自动分包,你的回调会被调用多次。所以缓冲区要能处理分包重组,或者干脆约定手机端每次不超过 20 字节。
/* BLE 写回调:把数据塞进环形缓冲区 */ void user_gatt_write_cb(uint8_t *data, uint16_t len) { for (uint16_t i = 0; i < len; i++) { ringbuf_put(&uart_tx_ring, data[i]); } /* 触发 UART DMA 发送,非阻塞 */ uart_start_tx_dma(&uart_tx_ring); }3.3 数据从 UART 到 BLE 的搬运
反方向就更好理解了。RA4M2 通过 UART 发来的数据,在 DA14531 的 UART 接收回调里被拿到,然后通过 Notify 推给手机。这里的关键是Notify 的触发频率。
BLE 的 Notify 不是你想发就能立刻发的,它受连接间隔(Connection Interval)限制。连接间隔一般是 7.5ms 到 4s,手机端可以协商。如果你 UART 收到数据的速度远快于连接间隔,数据就会堆积。我的处理方式是:UART 收到数据先入环形缓冲区,然后在一个定时器或者连接事件回调里检查缓冲区,有数据就打包 Notify 出去。
/* UART 接收回调:入缓冲区 */ void user_uart_rx_cb(uint8_t byte) { ringbuf_put(&ble_tx_ring, byte); } /* 连接事件回调:检查并发送 */ void user_conn_event_cb(void) { uint8_t buf[20]; uint16_t len = ringbuf_read(&ble_tx_ring, buf, sizeof(buf)); if (len > 0) { ble_notify_send(buf, len); } }提示:Notify 单包最大长度也受 MTU 限制。如果数据量大,要么协商更大的 MTU(手机端和模组端都支持的话可以到 247 字节),要么在应用层做分包。我一般把 MTU 协商到 247,这样一包能发 244 字节,效率高很多。
3.4 双向同时通信的冲突处理
双向透传最怕的是两边同时猛发,UART 和 BLE 都堵住。我实测下来,只要做好缓冲和流控,115200 的 UART 配 7.5ms 连接间隔,跑满也就几 KB/s,远没到瓶颈。真正会出问题的是缓冲区溢出。
我的做法是给两个方向各配一个 512 字节的环形缓冲区,并且在缓冲区快满的时候主动丢包或者反压。反压的意思是:如果 BLE 到 UART 的缓冲区快满了,就暂时不订阅 Notify 或者让手机端暂停发送;如果 UART 到 BLE 的缓冲区快满了,就通过流控信号让 RA4M2 暂停发送。这套机制加上去之后,连续跑几个小时都没再丢过数据。
4. 固件烧录与调试实战
4.1 DA14531 固件怎么烧进去
这是整个项目里最容易卡住新手的环节。DA14531 不像 STM32 那样插上 ST-Link 就能下载,它需要专门的工具和流程。官方提供两种方式:JTAG 和 UART。JTAG 需要额外的调试器,UART 下载则只需要一个 USB 转串口模块。
UART 下载的流程是这样的:先把 DA14531 的特定引脚拉到一个特定状态进入下载模式,然后用官方的 SmartSnippets Toolbox 通过串口把固件写进去。具体引脚和时序在芯片数据手册的"Boot"章节有详细说明,不同封装可能略有差异,一定要对着手册操作。
我用的是 FT231X 做的 USB 转串口模块,驱动装好之后在 SmartSnippets 里能识别到串口。下载的时候要注意波特率,官方工具默认用 57600 或者 115200,如果连不上就换一个试试。另外下载完成后要把引脚恢复到正常运行状态,否则芯片会一直停在下载模式。
注意:DA14531 的固件分两部分,一部分是协议栈(ROM 里已有),一部分是你的应用代码。烧录的时候只需要烧应用代码,但要注意代码的起始地址和大小限制,超了会烧不进去或者跑不起来。
4.2 用串口助手验证 UART 链路
在调 BLE 之前,我强烈建议先把 UART 链路单独验证通。方法很简单:RA4M2 端写一个循环发送的程序,每隔一秒发一串固定数据;DA14531 端先不跑 BLE,只跑一个 UART 回环程序,收到什么就原样发回。用串口助手接在 RA4M2 的调试串口上,看能不能收到回环数据。
这一步能排除掉大部分硬件问题:接线错了、波特率不对、电平不匹配、流控接反了,都会在这一步暴露出来。我见过太多人一上来就调 BLE,结果折腾半天发现是 TX/RX 接反了。先把 UART 调通,后面会顺很多。
4.3 手机端调试工具的选择
手机端我一般用两类工具:一类是通用的 BLE 调试 App,能扫描、连接、看服务、读写特征值;另一类是自己写的小程序或者 App,用来做最终的产品验证。通用工具推荐用 nRF Connect 或者 LightBlue,这两个在 iOS 和 Android 上都有,功能全,能看到原始的 GATT 结构。
调试的时候,先用通用工具连上 DA14531,确认服务 UUID 和特征值 UUID 都对,然后手动往"下行特征值"写几个字节,看 RA4M2 那边能不能收到。反过来,让 RA4M2 发数据,看"上行特征值"的 Notify 能不能收到。两边都通了,再上自己写的 App。
| 调试阶段 | 工具 | 验证目标 |
|---|---|---|
| UART 链路 | 串口助手 | 收发是否正常 |
| BLE 连接 | nRF Connect | 服务和特征值是否正确 |
| 下行通路 | nRF Connect 写特征值 | RA4M2 能否收到 |
| 上行通路 | nRF Connect 订阅 Notify | 手机能否收到 |
| 整体联调 | 自研 App | 双向透传是否稳定 |
4.4 功耗实测与优化
DA14531 的卖点是低功耗,但如果你代码写得不对,功耗照样下不来。我实测过几种状态:正常运行且连接保持时,平均电流大概几百微安;进入深睡眠后能降到几微安。差距主要来自几个地方。
一是广播间隔。如果设备一直广播,功耗会高不少。产品化的时候一般是连接后停止广播,断开后再重新广播。二是连接间隔。连接间隔越短,射频活动越频繁,功耗越高。7.5ms 是最短,但没必要,我一般协商到 30ms 或者 50ms,透传完全够用,功耗能降一大截。三是UART 空闲。UART 不用的时候要关掉或者进低功耗模式,否则它自己也会耗电。
/* 请求更长的连接间隔以降低功耗 */ void user_request_conn_param(void) { struct gap_conn_param param = { .intv_min = 24, /* 30ms */ .intv_max = 40, /* 50ms */ .latency = 4, /* 从设备延迟 */ .timeout = 400 /* 4s 超时 */ }; app_param_update_request(¶m); }从设备延迟(Slave Latency)这个参数很关键。它允许从设备跳过若干个连接事件不响应,从而省电。比如延迟设成 4,意味着从设备可以连续跳过 4 个连接事件,第 5 个才必须响应。透传场景下数据不是每时每刻都有,适当加大延迟能显著降低平均功耗。
5. 常见问题与排查速查
5.1 连接不上或者连上就断
这是最高频的问题。排查顺序我一般是这样的:先看广播有没有发出来,用手机扫描能不能看到设备名;能看到但连不上,检查广播数据里的服务 UUID 是否正确;能连上但马上断,多半是连接参数协商失败或者协议栈回调里有阻塞操作。
我遇到过一次连上就断,查了半天发现是 UART 回调里调用了printf,而printf是阻塞的,把协议栈卡死了。把printf去掉换成非阻塞的日志输出就好了。这个坑很典型,任何在 BLE 回调里做阻塞操作的行为都会导致连接异常。
5.2 数据丢包或者乱码
丢包和乱码要分开看。乱码通常是波特率不匹配或者电平问题,先确认两边波特率一致,再用示波器看波形是否干净。丢包则多半是缓冲区溢出或者流控没做好。
我整理了一个速查表,按现象对原因:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 偶发乱码 | 波特率误差大 | 测实际波特率,换时钟源 |
| 连续乱码 | 电平不匹配 | 量 TX/RX 电压 |
| 偶发丢包 | 缓冲区溢出 | 加大缓冲区,开流控 |
| 大量丢包 | 流控未启用 | 接 RTS/CTS 并配置 |
| 固定位置丢包 | 帧格式解析错 | 检查帧头和长度字段 |
5.3 手机搜不到设备
手机搜不到 DA14531,先确认模组在广播。可以用另一个手机或者 BLE 调试工具交叉验证。如果确认在广播但某个手机搜不到,可能是手机端缓存了之前的配对信息,去蓝牙设置里忽略该设备再重新搜。Android 上还有个常见问题是位置权限没开,某些版本不授权位置权限就扫不到 BLE 设备。
5.4 功耗比预期高很多
功耗高先量电流,看是平均电流高还是峰值电流高。平均电流高通常是连接间隔太短或者广播没停;峰值电流高可能是射频发射功率设太大了。DA14531 的发射功率可以调,默认可能是 0dBm,如果通信距离够用,降到 -6dBm 甚至更低能省不少电。
另外检查一下有没有引脚悬空。悬空的 IO 在输入状态下会因为电平不确定而反复翻转,产生额外功耗。不用的引脚要么设成输出低,要么使能内部上下拉。
5.5 固件烧录失败
烧录失败最常见的原因是没进下载模式。DA14531 进下载模式需要特定引脚在复位时保持特定电平,时序不对就进不去。建议用官方推荐的电路,把下载模式的控制引脚引出来,用跳线帽或者按键控制,比飞线可靠得多。
还有一个原因是串口被占用。SmartSnippets 打开串口后,如果串口助手还开着同一个口,就会冲突。烧录前把所有可能占用串口的软件都关掉。
6. 一些实操心得与扩展思路
6.1 关于代码分层的一点体会
这个项目做下来,我最大的体会是分层要清晰。RA4M2 端的代码我只关心业务逻辑和 UART 收发,完全不碰蓝牙;DA14531 端的代码我只关心 BLE 事件和 UART 搬运,完全不碰业务。两边通过一个简单的帧协议通信,谁改了都不影响对方。这种解耦在后期改需求的时候特别省心。
如果你把业务逻辑写到 DA14531 里,或者把蓝牙协议处理写到 RA4M2 里,短期看好像少了一层转发,长期看就是灾难。改一个功能要动两个芯片的代码,调试的时候还不知道是哪边的问题。
6.2 帧协议设计的小技巧
前面提到的0xAA 0x55 + 长度 + 负载 + 校验这个格式,我在好几个项目里都用过,很稳。这里补充两个细节:一是长度字段建议用两字节,支持更大的包;二是校验和可以用简单的累加和,也可以用 CRC16,看你对可靠性的要求。透传场景累加和一般够用,如果环境干扰大就上 CRC16。
还有一个技巧是转义。如果负载里可能出现和帧头相同的字节,要么用长度字段切分(我用的就是这种),要么做字节转义(比如把0xAA转成0xAA 0x00)。长度字段切分更高效,但要求长度字段本身不能出错;转义更鲁棒,但会增加数据量。我一般用长度字段,配合校验和,出错概率极低。
6.3 后续可以怎么扩展
这套框架搭好之后,扩展起来很灵活。比如你想加 OTA 升级,可以在 BLE 服务里加一个 OTA 特征值,手机把固件分包发过来,DA14531 收到后写到 Flash,重启后从新固件启动。再比如你想加多路数据,可以在帧协议里加一个通道号字段,RA4M2 端按通道分发,手机端按通道解析。
如果数据量再大,可以考虑把 UART 换成 SPI。SPI 速率比 UART 高一个数量级,但接线多几根,而且 DA14531 的 SPI 和 UART 可能共用引脚,要重新规划。透传场景 UART 一般够用,除非你要传音频或者图像。
最后分享一个我在调试时常用的小方法:在 RA4M2 和 DA14531 之间的 UART 线上并一个逻辑分析仪,抓原始字节流。BLE 那边用手机抓包,两边时间戳对齐,就能精确定位数据是在哪一段丢的。这个方法比盲猜高效得多,尤其是排查偶发问题时。
提示:逻辑分析仪抓 UART 的时候,采样率要至少是波特率的 10 倍以上,115200 的话建议 2MHz 以上采样,否则波形会糊,解码容易出错。
这套 RA4M2 加 DA14531 的方案,我从选型到调通大概花了两周,其中大部分时间耗在 DA14531 的 SDK 熟悉和烧录流程上。一旦跑通,后面加功能就很快了。如果你也在做类似的东西,建议先把 UART 链路和烧录流程这两个基础打牢,后面的 BLE 调试会顺利很多。