最近在做一个物联网网关项目,主控选了瑞萨的 RA4M2,蓝牙从机选了 Dialog 的 DA14531,两个芯片之间用 UART 协议通信,把数据双向透传到手机端 BLE。这整套链路做完,踩了不少坑,也总结出一套可以复用的调试方法。这篇就完整记录一下,从 UART 驱动的配置到底层透传逻辑的实现,再到手机端的联调与问题排查,希望能给正在做类似低功耗蓝牙透传项目的朋友一些参考。
这个方案的本质,是让 RA4M2 专注业务逻辑和数据处理,DA14531 只负责 BLE 无线链路,二者之间靠 UART 搭一条“数据管道”。为什么要这样拆?因为在实际项目中,单芯片方案要么算力不够,要么蓝牙协议栈把主控资源吃得太狠,要么射频性能和功耗没法兼顾。把无线部分独立出去后,主控选型自由度很大,软件迭代也不受影响。适合正在做传感器采集、智能家居、可穿戴设备、工业数据采集这类项目的朋友参考。
1. 方案选型与整体架构设计
1.1 为什么用“双芯片”而不是单芯片
很多人第一反应是:现在集成 BLE 的 MCU 一堆,为什么非要 RA4M2 加 DA14531 这么组合?这个选择背后的逻辑,其实是项目需求倒逼出来的。
RA4M2 是瑞萨的 Cortex-M33 内核 MCU,主频 100MHz,外设丰富,FSP(Flexible Software Package)这套软件框架用起来体验不错。但它自身没有射频单元,没法直接跑 BLE。如果要选带 BLE 的型号,比如 RA4M2 系列里某些内置蓝牙的衍生版本,或者直接用其他集成方案,就会遇到几个现实问题:
- 主控和射频在同一颗芯片上,天线布局和 PCB 走线对整体性能影响很大,射频调试的难度会被放大。
- BLE 协议栈要占不少 Flash 和 RAM,留给业务代码的空间变少。
- 如果项目后期要升级蓝牙版本、换更低功耗的射频方案,主控代码会被牵连,改动成本高。
而 DA14531 是 Dialog 的超低功耗蓝牙 SoC,Cortex-M0+ 内核,主打 IoT 低功耗市场,Sleep 模式功耗极低。它自身也能跑应用逻辑,但 Flash 和 RAM 都很小,不适合承载太复杂的业务。把它当作一颗“无线透传芯片”来用,恰好发挥它的长处。
双芯片架构最大的收益,是让“主控逻辑”和“无线链路”两个维度彻底解耦。主控想换 Flash 容量、加外设、升级性能,跟蓝牙没关系;蓝牙想升级协议栈、调整功耗策略,也只需要改从机端代码。调试的时候还能分头验证:先用 USB 转串口把两边 UART 打通,再单独测 BLE 链路,最后再合起来联调,问题定位路径非常清晰。
1.2 整条链路上的数据流与角色拆分
我习惯把这条链路拆成三段来看:
第一段是上行方向,从 RA4M2 到手机:RA4M2 的 UART TX 发数据给 DA14531 的 UART RX,DA14531 把数据打包成 GATT Notify 发送,手机端通过蓝牙服务读取。
第二段是下行方向,从手机到 RA4M2:手机通过 GATT Write 写数据给 DA14531,DA14531 的 UART TX 把数据转发给 RA4M2 的 UART RX。
第三段是 DA14531 两端的数据中转。它一边是 UART 物理接口,另一边是 BLE 的 GATT 服务,中间就是一个双向转发的缓冲区。
这个架构里,UART 协议是“本地数据管道”,BLE 特征值是“无线数据管道”。两个管道的速率不一样,UART 一旦定了波特率就是恒定的,而 BLE 的吞吐受连接间隔、MTU、从机状态等因素影响,波动很大。设计缓冲区和流控逻辑的时候,必须把这两条管道放在一起考虑,否则丢包是必然的。
1.3 UART 通信协议怎么定才靠谱
两个芯片之间的 UART 通信,不能想怎么发就怎么发,必须约定一个轻量级帧格式。我之前做过的方案是:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 1 字节 | 固定 0xAA |
| 长度 | 2 字节 | 负载长度,小端模式 |
| 负载 | N 字节 | 实际数据 |
| 校验 | 1 字节 | 累加和校验 |
帧头选 0xAA 是因为二进制里它是 10101010,接收端在乱码环境下更容易做同步。长度字段用 2 字节,足够支持最长 65535 字节的单帧数据。校验用累加和就够了,因为 UART 链路本身非常可靠,出错主要靠 BLE 和更上层的协议兜底,没必要上 CRC32 这种重型校验。
波特率我选了 115200。这个值的优势是所有开发板、USB 转串口芯片默认都支持,调试工具不用额外配。而且 115200 对应的字节速率约 11.5KB/s,已经远超 BLE 实际能跑通的稳定吞吐,短期内不会成为链路的瓶颈。如果你自己用的是内部 RC 振荡器且精度不高,建议降到 57600 更稳,后面我详细说原因。
2. UART 驱动开发实战
2.1 RA4M2 侧:FSP 配置 SCI 通道
RA4M2 的 UART 外设叫 SCI(Serial Communications Interface),FSP 里配置的方式和 STM32 的 CubeMX 类似。打开 FSP 后,在 Stacks 里添加一个 UART Stack,选择 SCI 通道,然后配置基础参数:
| 参数 | 推荐值 |
|---|---|
| Baud Rate | 115200 |
| Data Bits | 8 |
| Parity | None |
| Stop Bits | 1 |
| Flow Control | None |
| Interrupt | Enabled |
有一点要注意:接收中断和发送完成中断是两回事。FSP 里 UART 的收发回调是同一个回调函数,通过 event 区分。如果你的程序里接收数据量很大,建议接收全部走中断,发送可以走轮询,或者用 DMA 发送大块数据。我在这个项目里发送数据量小,统一用中断就够了。
配置完成后,打开串口并设置回调:
uart_ctrl_t g_uart0_ctrl; const uart_cfg_t g_uart0_cfg = { .baudrate = 115200, .data_bits = UART_DATA_BITS_8, .parity = UART_PARITY_NONE, .stop_bits = UART_STOP_BITS_1, .p_callback = uart0_callback, }; void uart0_callback(uart_callback_args_t *p_args) { if (p_args->event == UART_EVENT_RX_CHAR) { uint8_t ch = (uint8_t)p_args->data; ring_buffer_write(&rx_ring, ch); } }回调里拿到单字节数据后,绝不在这里做复杂处理,只往环形缓冲区里塞。中断回调里耗时太长的后果,就是系统中断被占死,蓝牙那边一旦有更高优先级的中断就要出问题。
2.2 DA14531 侧:UART 的 GPIO 映射与低功耗模式
DA14531 的 UART 引脚步配置要留意,它不像 STM32 那样一个引脚单一功能,而是通过 PID(Pin ID)把引脚映射到具体外设。某个引脚必须配置成 PID_UART1_TX、PID_UART1_RX 才能作为串口使用。这个含义是:引脚本身要复用成外设角色。
我用的 DA14531 引脚配置方式如下:
GPIO_ConfigurePin(GPIO_PORT_0, GPIO_PIN_5, OUTPUT, PID_UART1_TX, false); GPIO_ConfigurePin(GPIO_PORT_0, GPIO_PIN_6, INPUT, PID_UART1_RX, false);这里会踩一个大坑:DA14531 的工作电压和 IO 电平不一定和 RA4M2 匹配。DA14531 的 IO 供电需要看你的电源方案,如果 IO 电平是 1.8V,而 RA4M2 的 IO 是 3.3V,直接连上去短期能用,长期会有信号完整性和电平兼容风险。稳妥的办法是在中间加电平转换,或者至少确认双方 IO 是否兼容。
另外一个坑是 DA14531 进入 Extended Sleep 低功耗模式后,UART 不一定会保持接收状态。我实测下来,想让它在睡眠时还能被 UART 数据唤醒,需要额外配置唤醒引脚,或者用周期性的定时器去检测外部是否有数据。如果没有这层机制,你会发现整机睡眠后手机发来的数据全部丢失,RA4M2 那边收不到任何反应。
2.3 波特率匹配与信号完整性
很多人以为波特率设成一样就能通信,实际上两边芯片的系统时钟频率不同会带来误差。RA4M2 的高精度外部晶振通常在 10ppm 以内,DA14531 如果用了内部 RC 振荡器,误差可能达到百分之几。波特率一旦超过一定范围,就可能出现偶发乱码。
我建议在项目初期就把示波器接上,测量两边 UART TX 引脚的实际波形。一个简单的验证方法是:DA14531 主动周期发送固定字符 0x55,RA4M2 收到后回显,上位机统计误码率。0x55 的二进制是 01010101,波形上看就是一串方波,特别容易判断起始位和停止位的时序是否正常。
如果两边的晶振精度没办法保证,可以把波特率降到 57600 或者更低,系统稳定性立刻提升。项目里如果只是传传感器命令、控制指令这种小包数据,完全没必要追求高波特率,稳定压倒一切。
2.4 环形缓冲区与粘包处理
UART 是流式协议,应用层没有天然的“帧”边界,所以拆包处理是必做的。我的做法是维护一个静默期判断机制:收到第一个字节后启动 3 个字节时间的超时窗口,如果窗口内没有新数据到达,就认为一帧结束,把缓冲区的数据交给上层解析。
#define UART_FRAME_GAP_US 1000 void uart_frame_process(void) { if (ring_buffer_count(&rx_ring) > 0) { uint8_t ch = ring_buffer_read(&rx_ring); frame_buf[frame_len++] = ch; last_rx_time = get_tick_us(); } if (frame_len > 0 && (get_tick_us() - last_rx_time) > UART_FRAME_GAP_US) { protocol_parse(frame_buf, frame_len); frame_len = 0; } }这种“静默期拆包”方案特别适合不定长数据。固定长度帧当然也可以用 DMA 加空闲中断来做,但 DA14531 侧的逻辑简单,UART 发送的数据又多是传感器采集结果这类短数据,用静默期已经非常可靠。
2.5 Linux 下用 USB-UART 模块验证串口
联调之前,我习惯先在本机验证 RA4M2 的 UART 驱动有没有写对。方法很简单:拿一个 USB-UART 模块,用杜邦线把 TTL 的 TX、RX、GND 接好,插到电脑上。很多模块用的是 FT232R 或者 FT231X 芯片,Windows 下会自动装驱动,Linux 下会变成 /dev/ttyUSB0。
用 Python 的 pyserial 库做回环测试很方便:
import serial ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1) ser.write(b'hello ra4m2') data = ser.read(64) print(data)如果你的 UART 驱动配置正确,RA4M2 收到后会原样返回,电脑端能收到“hello ra4m2”。回环测试过了,再进入 DA14531 的联调,能省下很多排查时间。
3. DA14531 BLE 从机开发关键点
3.1 SDK 工程结构与必要配置
DA14531 的 SDK 是 Dialog 官方的 DA14531 SDK,工程结构里重点要关注这几个地方:
| 模块 | 作用 |
|---|---|
| user_config.h | 设备名称、连接参数、MTU 配置 |
| user_modules_config.h | 模块裁剪,关掉不用的功能来省 RAM |
| user_svc1_def.h | 自定义服务与特征值定义 |
| periph_setup.h | GPIO 和 UART 引脚配置 |
打开 SDK 后,第一步不是写代码,而是裁剪模块。SDK 默认打开了大量调试打印、日志功能、额外服务,如果全保留,Flash 和 RAM 都会比较紧张。因为项目只需要透传,我关掉了大部分非必要模块,只保留 UART、BLE 基础协议栈和自定义服务。
3.2 自定义服务与特征值设计
透传业务的核心是一个自定义服务,里面至少需要两个特征值:一个用于上行通知(RA4M2 → DA14531 → 手机),一个用于下行写入(手机 → DA14531 → RA4M2)。
| 特征值 | 属性 | 作用 |
|---|---|---|
| Tx Characteristic | Notify | DA14531 主动推数据给手机 |
| Rx Characteristic | Write | 手机写数据给 DA14531 |
这里有一个很容易忽略的点:写特征值有两种方式,Write(带响应)和Write Without Response(不带响应)。在透传场景里,建议把下行特征值设为 Write Without Response。因为手机端的一次写入需要等待从机回复的话,会显著降低吞吐量,尤其当你一次要发几百字节数据时,带响应模式会导致频繁等待、性能锐减。
MTU(最大传输单元)也要提前调整。默认 MTU 只有 23 字节,其中 3 字节被 ATT 头占用,实际一包只能传 20 字节。在 SDK 里把默认 MTU 配置成 247 后,配合数据包扩展特性,单包能传 244 字节,吞吐量提升明显。手机端如果支持,会在连接后自动发起 MTU 协商,你的从机只要在配置里允许就支持。
3.3 透传核心逻辑:两端缓冲区的协作策略
这里最关键的设计决策是:UART 收到数据后,是攒够多少字节再往 BLE 发,还是立即发?
我的策略是:UART 收到 1 字节就进 FIFO,然后判断当前 BLE 连接是否处于可发送状态、FIFO 里是否积累了足够数据。如果积累的数据超过 180 字节,攒着等下一次连接事件里一次性通过 Notify 发出去;如果积累的数据少,就攒够 1 包(例如 20 字节)再发。
原因是 BLE 的连接事件是周期性发生的(比如每 30ms 一个连接事件),每个事件里能发送的数据包数量有限。如果你一接到字节就发一个 Notify,等于强迫系统频繁调度,效率很低。攒成大数据包后,一次连接事件就能发完,吞吐量能提升好几倍。
下行方向相反:手机发来的数据包已经天然分包了(每个 GATT 包就是一段数据),DA14531 收到后直接塞进 UART 发送 FIFO 就行,不需要等。因为 BLE 的包速率本身不高,UART 115200 波特率能轻松跟得上。
3.4 代码骨架示例
DA14531 的 UART 接收回调大致是:
static void user_uart_receive_cb(uint8_t data) { if (transmit_fifo_write(data) == FIFO_OK) { if (fifo_count() >= 180) { user_send_transmit_data(); } } }GATT 写回调大致是:
static void user_svc1_write_handler(uint16_t att_idx, uint8_t *data, uint16_t len) { for (uint16_t i = 0; i < len; i++) { while (!uart_tx_byte(data[i])); } }注意上面这个串口发送不能放在中断上下文里太久,实际工程建议用队列接力,中断里只入队,主循环负责真正写 UART。
3.5 BLE 连接参数对透传的影响
连接参数是整个项目中隐性影响最大的一个配置项。DA14531 作为从机,连接间隔、从机延迟、超时时间都会直接影响收发数据的实时性和吞吐量。
| 参数 | 推荐值 | 影响 |
|---|---|---|
| 连接间隔 | 7.5ms ~ 15ms | 间隔越短,实时性越高,功耗越高 |
| 从机延迟 | 0 | 透传场景尽量设为 0,延迟大容易造成数据堆积 |
| 超时时间 | 2000ms | 太长断连响应慢,太短容易误判断连 |
我实测过:连接间隔设为 30ms 时,手机通过 Notify 收数据的稳定吞吐大约只有 1.5KB/s;把连接间隔调到 7.5ms,同时把 MTU 提升到 247 后,稳定吞吐能到 4KB/s 以上。而且这个吞吐差异在用户体验上是“能看视频”和“卡顿转圈”的天壤之别。
4. 联调与测试实践
4.1 手机端工具选型
调试 BLE 透传,我最推荐先别急着写 App,用现成工具验证链路通了再说。
- nRF Connect:Nordic 官方的手机调试工具,可以扫描、连接、浏览服务、读写特征值、开启通知,基本是 BLE 开发的标配。能看 Attr 数据,也方便查看 MTU 协商结果。
- Serial Bluetooth Terminal:安卓端一个可以直接连接 BLE 串口服务的 App,适合把手机当作一个透明的串口终端来用,直接发十六进制数或者字符串,收发都看得见。
联调初期,我会先用 Serial Bluetooth Terminal 连接设备,手动发几个字节,看 DA14531 是否转发到 RA4M2,RA4M2 是否回复。链路通了再上自己的 App,逻辑会清晰很多。
4.2 Wireshark 只抓指定 BLE 设备
如果调试中发现有些时间点数据丢了,或者是连接的时序不对,用手机日志可能难查,这时就得抓蓝牙报文。Wireshark 配合一个支持 BLE 嗅探的适配器(比如 nRF52832 Dongle 刷成 Sniffer 固件),可以抓到整个链路的空中报文。
Wireshark 里要只显示某个指定 BLE 设备,过滤条件可以这样写:
btatt && btle.advertising_address == xx:xx:xx:xx:xx:xx || btatt && btcommon.bd_addr == xx:xx:xx:xx:xx:xx这个过滤只保留你关心的那个设备相关的 ATT 层报文。实际排查中,我最常用的过滤条件是这样的:
btatt:查看所有 ATT 数据包(读写、通知)btl2cap:查看 L2CAP 层的 MTU 协商、分段重组情况btle:查看链路层连接事件、广播包
抓包的价值在于:你能从空中视角确认数据到底是手机发出去了、从机收到了但没转发,还是从机转了但手机没显示。有一次我查一个“偶发丢包”问题,抓包后发现手机发出 Write 命令后从机并没有回 Confirm,这说明数据卡在 DA14531 的协议栈侧,根本不是 UART 的问题。
4.3 长稳测试:1MB 大文件对拷
透传项目最怕的不是短连接能通,而是长时间跑数据后出现概率性丢包。我的长稳测试方法是:电脑串口接 RA4M2,通过 UART 发一份 1MB 的随机数据,手机端开启通知并把接收到的数据存成文件,测试结束后对比前后两个文件的 md5。
第一次跑这个测试的时候,等了十分钟发现文件 md5 对不上,而且丢块位置没有规律。后来定位到是 DA14531 的发送缓存区不够大,手机没有及时读取时,从机缓存满了就直接丢弃新数据。解决办法是把缓存区从 512 字节放大到 2048 字节,同时手机端改成用独立线程一直读,避免 UI 卡顿导致读取不及时。
这里真正值得注意的,是链路里最薄弱的环节往往不是你想的那个。BLE 空中的包、UART 的字节,都在各自的物理层有流量控制,但两个缓冲区之间的“交接”如果设计得不够好,丢包是既不带错误码、也不带重传标记的,极其隐蔽。
4.4 低功耗与掉电唤醒联调
项目里如果追求低功耗,DA14531 会进入 Extended Sleep,RA4M2 也可能进入 Deep Sleep。这种情况下双向唤醒是一个必须提前设计的点。
我遇到的情况是:DA14531 睡眠后,RA4M2 发来 UART 数据,DA14531 没有自动唤醒机制,数据全部丢失。后来配置了一个 GPIO 作为唤醒源,RA4M2 在发数据前先拉高这个 GPIO,让 DA14531 先醒来再发 UART 数据,问题解决。这个 GPIO 相当于一个“敲门”信号。
反过来,手机端写入数据时,DA14531 是被 BLE 协议栈唤醒了,但 RA4M2 还在睡眠,UART 数据到达后没人接收。RA4M2 侧的串口如果支持边沿唤醒中断就行,不支持的话,DA14531 也可以先通过一个 GPIO 拉高 RA4M2 的唤醒引脚,再发数据。两个芯片之间建立“握手”机制才是低功耗的稳妥方案。
5. 常见问题与排查实录
5.1 串口收到乱码
这是最常见、也最容易误判的问题。乱码的成因很多,我按概率排一下:
| 原因 | 典型表现 | 排查方法 |
|---|---|---|
| 波特率不匹配 | 一直接收但全是错误字符 | 示波器测波特率实际值 |
| 电平不匹配 | 偶尔能通,但长时间运行出错 | 分别测两芯片 IO 电压 |
| 供电不稳 | 高负载时乱码比例上升 | 用稳压电源直接供电测试 |
| 晶振精度差 | 短包正常,长包后段乱码 | 换有源晶振,降低波特率 |
| 地线未接 | 完全不通或随机乱码 | 检查 GND 是否连通 |
如果你用的是 USB-UART 模块,还要先确认模块的逻辑电平是 3.3V 还是 5V,很多模块出厂默认是 5V TTL,直接接 3.3V 系统会非常危险。
5.2 BLE 连接后过一段时间自动断开
这个问题的原因通常藏在连接参数配置里。如果超时时间太短,比如设为 200ms,当连接事件里偶尔被系统任务抢占,从机没能及时回复,主机会误判“从机失联”,主动断开连接。
我后来把超时时间稳定地配置在 2000ms,同时把从机延迟设为 0,连接稳定性立刻改善。如果设备处于低功耗状态,还要注意从机醒来后需要及时和主机重新同步,否则连接会因“假死”被主机断开。
另外,安卓手机和 iOS 的蓝牙栈行为差异较大。安卓设备在系统内存紧张或省电模式时会强制断开 BLE,iOS 的断开更多是超时导致。如果手机端频繁出现“已断开”提示,先用 nRF Connect 连接后保持不动观察是否还会断,能区分是协议栈问题还是 App 的问题。
5.3 双向并发吞吐量上不去
双向同时收发时的吞吐量往往比单向低。瓶颈通常在两个地方:
- UART 半双工效应:RA4M2 的 UART 如果 TX 和 RX 都是单线程轮询,就会出现发数据时无法及时收数据的窗口;
- BLE 单连接事件包数限制:连接事件里既要有 Notify 又要收 Write,每个事件的处理时间会被瓜分。
优化思路是:UART 侧用中断收发,配合双缓冲;BLE 侧尽量把多包 Notify 合并到一个连接事件里发,减少事件切换次数。
我实测的结果是:优化后双向同时收发时,上下行各约 2KB/s,已经满足大多数透传场景了。如果你的场景要求高吞吐,建议把 UART 波特率提到 460800,并开启 BLE 的 DLE(Data Length Extension),且用专门的 BLE 透传 SoC 方案,而不是通用低功耗 SoC。
5.4 手机收不到最后一段数据
这个问题很隐蔽,花了很久才定位。原因是:RA4M2 发完最后一段数据后立即把 DA14531 的 UART 接收线程停掉或者 DA14531 进入低功耗,FIFO 里的残留数据还没通过 Notify 送出去就被清掉了。
解决办法是在进入低功耗前加一个“排空逻辑”:UART FIFO 空闲且 BLE 的发送缓冲也清空后,才允许进入睡眠。或者让 RA4M2 发完数据后等待 DA14531 回一个 ACK 再断电。总之,数据完整性的保障,必须建立在明确的“数据已在无线链路送出”确认机制上。
5.5 常见问题速查表
| 问题 | 排查优先级 | 关键思路 |
|---|---|---|
| 串口乱码 | 波特率 → 电平 → 供电 | 先示波器测波形,再动软件 |
| UART 无数据 | 接线 → GPIO 复用 → 中断回调 | 用 USB-UART 回环验证驱动 |
| BLE 搜不到设备 | 广播开关 → 广播间隔 → 天线 | 关广播间隔的省电机制 |
| 连接后断连 | 连接参数 → 功耗模式 | 超时时间放宽到 2000ms |
| 数据丢包 | 缓冲区 → 排空逻辑 → MTU | 大缓存+明确 ACK 机制 |
| 低功耗唤醒失败 | GPIO 唤醒配置 | 发数据前先拉高唤醒引脚 |
| 手机端写入失败 | MTU → 特征值属性 | 用 Write Without Response |
6. 最后再分享一个调试顺序
我个人在整个项目里踩过最大的坑,就是一上来就同时调 UART 和 BLE,结果问题一旦出现,两边都像“黑盒”,完全不知道哪一路出错。后来我固定了一套调试顺序,效率提升非常明显:
第一步,静态回环。把 RA4M2 的 TX 接到 RX,用上位机发数据,验证串口外设配置有没有问题。
第二步,USB-UART 替代 DA14531。用 USB-UART 模块和 RA4M2 对接,电脑端直接模拟 DA14531 的发送行为,验证 RA4M2 的协议解析逻辑。
第三步,DA14531 回环测试。DA14531 串口接电脑,DA14531 收到数据后不进 BLE,直接原样回发,验证 DA14531 的 UART 驱动。
第四步,DA14531 串口对接 RA4M2。两个芯片直接 UART 互通,电脑端通过 USB-UART 监看数据,确认本地链路全通。
第五步,加 BLE。DA14531 和 RA4M2 不动,手机连接 DA14531,先测下行,再测上行,最后双向并发。
这套顺序的本质,是每次只引入一个新变量。几乎所有的疑难杂症,只要遵循“先串口、后蓝牙,先单向、后双向,先本地、后无线”的原则,都能把问题限定到一个很小的范围里。
另一个心得是:透传项目的性能瓶颈,往往不是 BLE 空中速率,而是 UART 与 BLE 缓冲区的配合策略。宁可让缓冲区大一点、发送节奏平滑一点,也不要一有数据就立刻推给协议栈,那是新手最容易犯的错。做透传做得多了就会明白,稳定的链路,比高吞吐更值钱。