SLIP 协议封装与解析详解:原理、C 代码与实战案例
> 适配嵌入式 IAR / ZYNQ 裸机项目,适用于 ICD 协议、PS 与 PL 之间 SLIP 报文通信场景。
目录
-
什么是 SLIP 协议
-
帧格式与转义规则
-
完整 C 语言实现(编码 / 解码)
-
与 CRC-16-IBM 结合完整流程
-
ZYNQ 实战:PS 接收 PL 的 SLIP 数据包
-
高频踩坑点与边界处理
-
测试用例参考表
1. 什么是 SLIP 协议
**SLIP(Serial Line Internet Protocol,串行线路因特网协议)** 是一种非常简单的串行线路数据封装协议,最早用于在串口上传输 IP 数据包。
在嵌入式领域(军工 ICD、ZYNQ PS/PL 通信、串口链路),SLIP 被广泛用作**面向字节流的帧封装协议**,解决两个核心问题:
-
**帧边界识别**:串口是连续字节流,没有"帧"概念,SLIP 用特殊字节标记帧的起始和结束。
-
**透明传输**:当数据内部出现特殊字节时,通过转义机制保证不干扰帧边界。
> SLIP 的最大优点:**实现极其简单,几乎零协议开销**。缺点是:无差错校验(需配合 CRC)、无压缩、无重传机制------所以工程上通常 **SLIP 负责组帧 + CRC 负责校验** 组合使用。
2. 帧格式与转义规则
2.1 特殊字节定义
| 字节 | 值 | 含义 |
| ---- | ---- | ---- |
| `END` | `0xC0` | 帧结束标记(也是帧起始标记) |
| `ESC` | `0xDB` | 转义标记 |
| `ESC_END` | `0xDC` | 转义后的 END(0xC0) |
| `ESC_ESC` | `0xDD` | 转义后的 ESC(0xDB) |
2.2 标准 SLIP 帧结构
```
+------+------------------+------+
| 0xC0 | 数据(已转义) | 0xC0 |
+------+------------------+------+
```
-
帧首尾各一个 `0xC0`。
-
中间的数据经过转义处理,保证内部不会出现裸的 `0xC0`。
2.3 转义规则(核心,务必记牢)
**编码(发送侧)**------数据中遇到特殊字节时替换:
| 原始数据字节 | 替换为 |
| ---- | ---- |
| `0xC0` | `0xDB 0xDC` |
| `0xDB` | `0xDB 0xDD` |
| 其他 | 原样保留 |
**解码(接收侧)**------遇到 `0xDB` 时看下一个字节:
| 收到的序列 | 还原为 |
| ---- | ---- |
| `0xDB 0xDC` | `0xC0` |
| `0xDB 0xDD` | `0xDB` |
> ⚠️ **重点**:转义对象是 **整帧数据(含 CRC 字节)**,而不是只对业务数据转义。CRC 本身可能算出 `0xC0` 或 `0xDB`,必须一并转义,否则帧边界会被破坏。
3. 完整 C 语言实现(IAR 裸机)
3.1 SLIP 编码(封装)
```c
#include <stdint.h>
#define SLIP_END 0xC0
#define SLIP_ESC 0xDB
#define SLIP_ESC_END 0xDC
#define SLIP_ESC_ESC 0xDD
/**
* @brief SLIP 编码:把原始报文封装成 SLIP 帧(含首尾 0xC0)
* @param src 原始报文(业务数据 + CRC,即完整待发数据)
* @param src_len 原始报文长度
* @param dst 输出缓冲区,需足够大(最坏为 src_len*2 + 2)
* @return 封装后的 SLIP 帧总长度
*/
uint16_t slip_encode(const uint8_t *src, uint16_t src_len, uint8_t *dst)
{
uint16_t out = 0;
dstout++ = SLIP_END; /* 帧首 0xC0 */
for (uint16_t i = 0; i < src_len; i++)
{
if (srci == SLIP_END)
{
dstout++ = SLIP_ESC;
dstout++ = SLIP_ESC_END;
}
else if (srci == SLIP_ESC)
{
dstout++ = SLIP_ESC;
dstout++ = SLIP_ESC_ESC;
}
else
{
dstout++ = srci;
}
}
dstout++ = SLIP_END; /* 帧尾 0xC0 */
return out;
}
```
3.2 SLIP 解码(解析)
解码采用**状态机 + 逐字节输入**方式,适合串口中断 / DMA 逐字节收包的场景。
```c
typedef enum
{
SLIP_STATE_WAIT_END, /* 等待帧首 0xC0 */
SLIP_STATE_DATA, /* 接收数据 */
SLIP_STATE_ESC /* 遇到转义符 0xDB */
} slip_state_t;
typedef struct
{
slip_state_t state;
uint8_t rx_buf1024; /* 接收缓冲区,按需调整 */
uint16_t rx_len; /* 已接收数据长度 */
} slip_rx_t;
/**
* @brief SLIP 逐字节解码
* @param rx SLIP 接收句柄
* @param byte 收到的原始字节
* @return 1 = 收到完整一帧(rx_buf 内为解码后数据),0 = 继续接收
* -1 = 缓冲区溢出错误
*/
int slip_decode_byte(slip_rx_t *rx, uint8_t byte)
{
switch (rx->state)
{
case SLIP_STATE_WAIT_END:
if (byte == SLIP_END)
{
rx->state = SLIP_STATE_DATA;
}
/* 帧首之前的多余字节直接丢弃 */
break;
case SLIP_STATE_DATA:
if (byte == SLIP_END)
{
/* 收到帧尾:完整一帧 */
rx->state = SLIP_STATE_WAIT_END;
return 1;
}
else if (byte == SLIP_ESC)
{
rx->state = SLIP_STATE_ESC;
}
else
{
if (rx->rx_len >= sizeof(rx->rx_buf))
{
rx->rx_len = 0;
rx->state = SLIP_STATE_WAIT_END;
return -1; /* 溢出 */
}
rx->rx_bufrx-\>rx_len++ = byte;
}
break;
case SLIP_STATE_ESC:
rx->state = SLIP_STATE_DATA;
if (byte == SLIP_ESC_END)
{
byte = SLIP_END;
}
else if (byte == SLIP_ESC_ESC)
{
byte = SLIP_ESC;
}
else
{
/* 非法转义序列:按错误处理,丢弃该帧 */
rx->rx_len = 0;
rx->state = SLIP_STATE_WAIT_END;
return -1;
}
if (rx->rx_len >= sizeof(rx->rx_buf))
{
rx->rx_len = 0;
rx->state = SLIP_STATE_WAIT_END;
return -1;
}
rx->rx_bufrx-\>rx_len++ = byte;
break;
}
return 0;
}
```
4. 与 CRC-16-IBM 结合完整流程
> 工程上 SLIP 不提供校验,必须搭配 CRC。推荐组合:**CRC-16-IBM 校验 + SLIP 组帧**。
> (CRC-16-IBM 实现见另一篇博客,参数 poly=0x8005 init=0xFFFF refin=true refout=true)
4.1 发包完整流程(发送侧)
```
业务数据 → CRC16 计算 → 数据+CRC 追加 → SLIP 转义封装 → 加 0xC0 帧首尾 → 串口/DMA 发送
```
-
组装 ICD 业务报文(原始业务数据)。
-
用 `crc16_ibm()` 计算业务数据 CRC。
-
将 CRC 小端追加到业务数据末尾,得到**原始完整报文**。
-
用 `slip_encode()` 对原始完整报文(含 CRC)整体转义,加帧首尾 `0xC0`。
-
通过串口 / DMA 发送。
```c
/* 发包示例 */
void slip_send_packet(const uint8_t *business_data, uint16_t data_len)
{
static uint8_t raw_pkt1024; /* 业务数据 + CRC */
static uint8_t slip_frame2048; /* SLIP 封装后 */
/* 1. 拷贝业务数据 */
memcpy(raw_pkt, business_data, data_len);
/* 2. 计算 CRC 并小端追加 */
uint16_t crc = crc16_ibm(business_data, data_len);
raw_pktdata_len + 0 = (uint8_t)(crc & 0xFF);
raw_pktdata_len + 1 = (uint8_t)((crc >> 8) & 0xFF);
/* 3. SLIP 封装(含 CRC 一起转义) */
uint16_t frame_len = slip_encode(raw_pkt, data_len + 2, slip_frame);
/* 4. 发送:串口 DMA 或轮询 */
uart_dma_send(slip_frame, frame_len);
}
```
4.2 收包完整流程(接收侧)
```
串口字节流 → 逐字节 SLIP 解码 → 还原原始报文 → 末尾2字节CRC → 校验 → 通过则解析业务,失败丢弃
```
-
串口中断 / DMA 每次收到 1 字节,调用 `slip_decode_byte()`。
-
返回 1 表示收到完整一帧,`rx_buf` 内为**解码后的原始报文**。
-
原始报文末尾 2 字节是 CRC,前面是业务数据。
-
用 `crc16_check_packet()` 校验,通过才解析业务,失败丢弃该帧。
```c
/* 收包示例 */
slip_rx_t g_rx = {0};
/* 串口中断里调用 */
void uart_isr_byte_received(uint8_t byte)
{
int ret = slip_decode_byte(&g_rx, byte);
if (ret == 1)
{
/* 收到完整一帧:g_rx.rx_buf 为解码后数据,g_rx.rx_len 为长度 */
if (g_rx.rx_len >= 2)
{
uint16_t data_len = g_rx.rx_len - 2;
uint16_t calc_crc = crc16_ibm(g_rx.rx_buf, data_len);
uint16_t recv_crc = ((uint16_t)g_rx.rx_bufdata_len + 1 << 8)
| g_rx.rx_bufdata_len;
if (calc_crc == recv_crc)
{
/* CRC 校验通过,处理业务数据 g_rx.rx_buf0..data_len-1 */
handle_business_frame(g_rx.rx_buf, data_len);
}
/* 校验失败:静默丢弃 */
}
g_rx.rx_len = 0; /* 复位,准备下一帧 */
}
else if (ret < 0)
{
g_rx.rx_len = 0; /* 溢出或非法转义,丢弃并复位 */
}
}
```
5. ZYNQ 实战:PS 接收 PL 的 SLIP 数据包
> 你的项目场景:PL 通过串口/内部互联把 SLIP 数据包发给 PS,PS 侧负责解码和校验。
5.1 关键设计要点
-
**DMA 还是轮询**:如果 PL 持续发送,建议用 **UART DMA + 空闲中断**,减少 CPU 占用;数据量小也可用轮询。
-
**逐字节喂给状态机**:无论 DMA 还是中断,最终都逐字节调用 `slip_decode_byte()`。
-
**接收缓冲够大**:SLIP 转义会使数据膨胀,但**解码后的原始报文长度 ≤ 帧数据长度**,所以接收缓冲按原始报文最大长度设置即可。
-
**复位时机**:每收完一帧或出错,立即清零 `rx_len`,防止脏数据污染下一帧。
5.2 PS 端收包伪代码
```c
void ps_slip_rx_task(void)
{
while (1)
{
uint8_t byte;
if (uart_receive_byte(&byte) == OK)
{
int ret = slip_decode_byte(&g_rx, byte);
if (ret == 1)
{
process_slip_frame(); /* 校验 + 业务解析 */
}
else if (ret < 0)
{
g_rx.rx_len = 0; /* 异常复位 */
}
}
}
}
```
6. 高频踩坑点与边界处理
| 场景 | 问题 | 正确做法 |
| ---- | ---- | ---- |
| 转义范围 | 只对业务数据转义,漏掉 CRC 字节 | CRC 会算出 `0xC0/0xDB`,必须 **连同 CRC 一起转义** |
| 计算 CRC 时机 | 直接对 SLIP 转义后的字节流算 CRC | 必须先解码还原原始报文,再用原始数据算 CRC |
| 解码复位 | 一帧结束后 rx_len 未清零 | 收到帧尾立即清零,防止残留脏数据 |
| 非法转义 | 收到 `0xDB` 后跟了非 `0xDC/0xDD` | 视为帧错误,丢弃整帧并复位 |
| 帧首前垃圾 | 帧首 `0xC0` 之前有多余字节 | 状态机自动丢弃,不进入数据区 |
| 缓冲区溢出 | 单帧超过缓冲 | 溢出即报错,丢弃该帧并复位 |
| 连续帧 | 两帧紧邻(前一帧尾即后一帧首) | 状态机天然支持:收到帧尾后自动进入等待帧首,下一字节 `0xC0` 即新帧首 |
6.1 边界技巧:用"帧首尾同字节"简化
SLIP 的 `END`(0xC0)既是帧首又是帧尾,因此**两个连续帧可以直接拼接**:
```
帧A 0xC0 ...data... 0xC0 帧B 0xC0 ...data... 0xC0
```
状态机收到 `0xC0` 时,如果当前在 DATA 态则是帧尾(完成一帧);如果在 WAIT_END 态则是新帧首。上述实现天然兼容,无需额外处理。
7. 测试用例参考表
7.1 转义测试
| 原始数据(HEX) | SLIP 编码后(HEX) |
| ---- | ---- |
| `C0` | `C0 DB DC C0` |
| `DB` | `C0 DB DD C0` |
| `01 02 03` | `C0 01 02 03 C0` |
| `C0 DB 01` | `C0 DB DC DB DD 01 C0` |
7.2 完整报文测试(含 CRC)
以业务数据 `02 63 06 00 07 02 01`(CRC=0x3C8D,小端 `8D 3C`)为例:
```
原始完整报文:02 63 06 00 07 02 01 8D 3C
SLIP 帧 :C0 02 63 06 00 07 02 01 8D 3C C0 (本例无 0xC0/0xDB,未触发转义)
```
> 若业务数据或 CRC 中出现 `0xC0` / `0xDB`,则会触发转义,帧长度相应膨胀。