最近在项目里需要直接处理 SBUS 原始数据,不是用现成飞控库,而是自己从串口读字节流解析。网上资料不少,但要么讲得太抽象,要么直接甩一段代码,对“为什么这么写”交代得不清楚。这里结合一次完整的实现过程,记录一下 SBUS 的结构以及在 Java 里怎么比较稳妥地解析,尤其是数据被截断的情况。
一、什么是 SBUS
SBUS 是 Futaba 提出的一种串行通信协议,主要用在遥控接收机和飞控之间。它的目标很简单:
用一根串口线,把多个遥控通道的数据快速、稳定地传过来。
从使用角度看,SBUS 本质上就是:
串口通信
固定帧格式
每一帧里塞了 16 个模拟通道 + 若干状态位
在无人机、机器人这些实时控制场景里,用得非常多。
二、SBUS 有什么特点
先说几个实际开发中比较容易踩坑的点。
1. 串口参数不太“常规”
SBUS 用的不是常见的 115200,而是:
波特率:100000
数据位:8
校验位:Even
停止位:2
也就是常说的 100000 8E2。
如果串口参数不对,后面解析逻辑写得再完美也没用。
2. 帧长度是固定的
一帧 SBUS 固定 25 字节,结构大致是:
第 0 字节:帧头,固定 0x0F
第 1~22 字节:通道数据
第 23 字节:状态位(失联、Failsafe、额外通道)
第 24 字节:帧尾,一般是 0x00
这点非常重要,后面处理截断数据时会反复用到。
SBUS 帧结构
一帧 SBUS 数据固定 25 字节:
状态字节(Byte 23)说明
3. 通道是按 bit 打包的
这是 SBUS 最“反人类”的地方。
一共有 16 个模拟通道
每个通道 11 bit
所有 bit 连续打包在 Byte1~Byte22 里
小端,按 bit 流排列
也就是说,一个通道的数据可能跨字节,甚至跨多个字节。
三、SBUS 数据应该怎么解析
如果你是直接从串口拿原始字节流,解析思路大概是这样:
串口不断回调字节数据
把数据先放进一个缓存
在缓存里找帧头 0x0F
确认后面是否至少还有 25 个字节
校验帧尾
解析通道和状态位
移除已经消费的数据,继续解析
这里有一个非常关键的前提:
永远不要假设一次 read 就是一整帧 SBUS。
四、为什么一定要考虑数据被截断
这是实际项目里一定会遇到的问题:
一次只读到 7 个字节
或者一次读到 60 个字节(两帧多)
中间插了一点噪声
串口短暂断开又恢复
如果代码里写的是:
read 25 bytes -> parse
那基本迟早会出问题。
正确的做法一定是:流式处理 + 帧同步。
五、Java 中解析 SBUS 的一个实现方式
下面这套实现不是追求“最优”,而是追求 稳定、好理解、好调试。
1. 通道 bit 解析逻辑
先把 25 字节中的通道部分解析成 16 个 int。
public class SbusParser {
public static int[] parseChannels(byte[] frame) {
int[] channels = new int[16];
int bitIndex = 0;
for (int ch = 0; ch < 16; ch++) {
int value = 0;
for (int i = 0; i < 11; i++) {
int byteIndex = 1 + (bitIndex >> 3);
int bitOffset = bitIndex & 0x07;
int bit = (frame[byteIndex] >> bitOffset) & 0x01;
value |= (bit << i);
bitIndex++;
}
channels[ch] = value;
}
return channels;
}
}
逻辑其实很直白:
把 Byte1~Byte22 当成一个连续的 bit 流
每 11 bit 还原一个通道值
2. 处理截断和粘包的核心逻辑
重点在这里。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class SbusFrameDecoder {
private static final int FRAME_LENGTH = 25;
private final List<Byte> buffer = new ArrayList<>();
public void onDataReceived(byte[] data) {
for (byte b : data) {
buffer.add(b);
}
decode();
}
private void decode() {
while (buffer.size() >= FRAME_LENGTH) {
// 先对齐帧头
if ((buffer.get(0) & 0xFF) != 0x0F) {
buffer.remove(0);
continue;
}
// 数据还不够一帧,等下一次
if (buffer.size() < FRAME_LENGTH) {
return;
}
// 校验帧尾
if ((buffer.get(24) & 0xFF) != 0x00) {
buffer.remove(0);
continue;
}
// 拿出完整一帧
byte[] frame = new byte[FRAME_LENGTH];
for (int i = 0; i < FRAME_LENGTH; i++) {
frame[i] = buffer.remove(0);
}
handleFrame(frame);
}
}
private void handleFrame(byte[] frame) {
int[] channels = SbusParser.parseChannels(frame);
boolean frameLost = (frame[23] & 0x04) != 0;
boolean failsafe = (frame[23] & 0x08) != 0;
System.out.println(Arrays.toString(channels));
System.out.println("failsafe = " + failsafe);
}
}
这段代码的核心思想只有一句话:
不完整的数据先留着,异常数据直接丢,永远从帧头开始同步。
六、一些实践中的建议
结合踩过的坑,给几点建议:
不要依赖“读取长度等于 25”
帧头不同步时,果断丢字节
解码逻辑和业务逻辑分开
出现 failsafe / frame lost 时要打日志
调试阶段可以把原始字节 dump 出来对照分析
七、总结
SBUS 本身并不复杂,复杂的是:
bit 级打包
串口流式数据的不确定性
只要在设计上接受“数据随时可能不完整”这个事实,解析逻辑就会变得很清晰。
这套思路不只适用于 SBUS,对其他任何 固定帧串口协议 都一样适用。