SLIP 协议封装与解析详解:原理、C 代码与实战案例

SLIP 协议封装与解析详解:原理、C 代码与实战案例

> 适配嵌入式 IAR / ZYNQ 裸机项目,适用于 ICD 协议、PS 与 PL 之间 SLIP 报文通信场景。


目录

  1. 什么是 SLIP 协议

  2. 帧格式与转义规则

  3. 完整 C 语言实现(编码 / 解码)

  4. 与 CRC-16-IBM 结合完整流程

  5. ZYNQ 实战:PS 接收 PL 的 SLIP 数据包

  6. 高频踩坑点与边界处理

  7. 测试用例参考表


1. 什么是 SLIP 协议

**SLIP(Serial Line Internet Protocol,串行线路因特网协议)** 是一种非常简单的串行线路数据封装协议,最早用于在串口上传输 IP 数据包。

在嵌入式领域(军工 ICD、ZYNQ PS/PL 通信、串口链路),SLIP 被广泛用作**面向字节流的帧封装协议**,解决两个核心问题:

  1. **帧边界识别**:串口是连续字节流,没有"帧"概念,SLIP 用特殊字节标记帧的起始和结束。

  2. **透明传输**:当数据内部出现特殊字节时,通过转义机制保证不干扰帧边界。

> 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 发送

```

  1. 组装 ICD 业务报文(原始业务数据)。

  2. 用 `crc16_ibm()` 计算业务数据 CRC。

  3. 将 CRC 小端追加到业务数据末尾,得到**原始完整报文**。

  4. 用 `slip_encode()` 对原始完整报文(含 CRC)整体转义,加帧首尾 `0xC0`。

  5. 通过串口 / 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 → 校验 → 通过则解析业务,失败丢弃

```

  1. 串口中断 / DMA 每次收到 1 字节,调用 `slip_decode_byte()`。

  2. 返回 1 表示收到完整一帧,`rx_buf` 内为**解码后的原始报文**。

  3. 原始报文末尾 2 字节是 CRC,前面是业务数据。

  4. 用 `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 关键设计要点

  1. **DMA 还是轮询**:如果 PL 持续发送,建议用 **UART DMA + 空闲中断**,减少 CPU 占用;数据量小也可用轮询。

  2. **逐字节喂给状态机**:无论 DMA 还是中断,最终都逐字节调用 `slip_decode_byte()`。

  3. **接收缓冲够大**:SLIP 转义会使数据膨胀,但**解码后的原始报文长度 ≤ 帧数据长度**,所以接收缓冲按原始报文最大长度设置即可。

  4. **复位时机**:每收完一帧或出错,立即清零 `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`,则会触发转义,帧长度相应膨胀。

相关推荐
꯭自꯭闭꯭1 小时前
达梦SQL优化相关
linux·运维·数据库·sql
海上小飞龙1 小时前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
青少儿编程课堂1 小时前
后缀自动机解析:本质不同子串与最长重复片段统计
c++·python·算法·bfs·信息学竞赛
朝朝辞暮i1 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法
狗凯之家源码网2 小时前
淘客口令落地页生成器 PHP 版深度评测
开发语言·php
一条破秋裤2 小时前
Linux 共享内存通信:POSIX 共享内存与 mmap
linux·运维·服务器
画绛集美术2 小时前
本地化部署LLM用于美术史问答:一间美术教室的实践记录
开发语言·php
八解毒剂2 小时前
【计组】中央处理器CPU
java·开发语言·计算机组成原理
小灰灰搞电子2 小时前
C语言 <ctype.h> 头文件详解
c语言·ctype.h