SBUS 协议解析实践:从原始数据到 Java 解码

SBUS 协议解析实践:从原始数据到 Java 解码

最近在项目里需要直接处理 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 字节

字节索引

内容

0

帧头 0x0F

1~22

通道数据(16 个通道,11bit/通道)

23

状态字节

24

帧尾 0x00

状态字节(Byte 23)说明

Bit

含义

bit0

Channel 17(数字通道)

bit1

Channel 18(数字通道)

bit2

Frame Lost

bit3

Failsafe

bit4~7

保留


3. 通道是按 bit 打包的

这是 SBUS 最“反人类”的地方。

  • 一共有 16 个模拟通道

  • 每个通道 11 bit

  • 所有 bit 连续打包在 Byte1~Byte22 里

  • 小端,按 bit 流排列

也就是说,一个通道的数据可能跨字节,甚至跨多个字节


三、SBUS 数据应该怎么解析

如果你是直接从串口拿原始字节流,解析思路大概是这样:

  1. 串口不断回调字节数据

  2. 把数据先放进一个缓存

  3. 在缓存里找帧头 0x0F

  4. 确认后面是否至少还有 25 个字节

  5. 校验帧尾

  6. 解析通道和状态位

  7. 移除已经消费的数据,继续解析

这里有一个非常关键的前提:
永远不要假设一次 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,对其他任何 固定帧串口协议 都一样适用。

frp内网穿透 2026-01-15

评论区