기술 가이드

UART SLIP 프레이밍: END·ESC 이스케이프와 수신 상태 머신 검증

RFC 1055의 END·ESC 바이트 스터핑을 C로 구현하고, 연속 UART 수신에서 손상·초과 프레임을 거르는 방법을 검증합니다.

목차

UART는 바이트의 흐름만 전달합니다. 한 번의 read나 DMA 콜백이 메시지 하나와 일치한다고 가정하면, 두 메시지가 붙거나 하나가 나뉘어 들어올 때 경계가 흐려집니다. RFC 1055의 SLIP은 END 바이트로 프레임을 나누고, 데이터에 같은 값이 있을 때 이스케이프하는 간단한 방식을 설명합니다. MCU와 호스트 사이에서 프레임 경계를 시험할 때도 이 규칙을 활용할 수 있습니다.

네 가지 바이트와 선로의 실제 값

이름 16진수 의미
END C0 프레임 끝
ESC DB 다음 바이트를 이스케이프
ESC_END DC ESC DC를 데이터 C0으로 복원
ESC_ESC DD ESC DD를 데이터 DB로 복원

데이터 11 C0 DB 22는 C0 11 DB DC DB DD 22 C0으로 보냅니다. 앞의 C0은 RFC가 소개한 시작 END 관례로, 앞서 쌓인 잡음을 버리는 데 도움이 됩니다. 연속된 END가 만든 길이 0 프레임은 무시합니다. 최대 인코딩 길이는 데이터 길이의 두 배에 시작·끝 바이트 2개를 더한 값이므로 송신 버퍼 크기를 먼저 계산하세요.

바이트 단위 수신 상태 머신

다음 C11 예제는 인코딩과 수신기를 함께 보여줍니다. 수신기는 시작 END를 기다리고, ESC 뒤에 허용되지 않은 값이 오거나 8바이트 예제 버퍼를 넘으면 해당 프레임을 끝 END까지 버립니다. 이 엄격한 오류 처리와 8바이트 한도는 이 예제의 설계 정책입니다. 실제 SLIP/IP 패킷에 8바이트 버퍼를 사용하지 마세요. RFC 1055는 기존 구현의 1006바이트 수신 권고를 설명합니다. END 뒤에서 잘린 ESC도 손상 프레임으로 처리합니다.

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

enum { END = 0xC0, ESC = 0xDB, ESC_END = 0xDC, ESC_ESC = 0xDD };

/* Returns bytes written, or zero when the output buffer is too small. */
static size_t slip_encode(const uint8_t *src, size_t count,
uint8_t *dst, size_t capacity) {
size_t used = 0;
if (capacity == 0) return 0;
dst[used++] = END; /* discard any prior line noise */
for (size_t i = 0; i < count; ++i) {
if (src[i] == END || src[i] == ESC) {
if (capacity - used < 2) return 0;
dst[used++] = ESC;
dst[used++] = src[i] == END ? ESC_END : ESC_ESC;
} else {
if (capacity == used) return 0;
dst[used++] = src[i];
}
}
if (capacity == used) return 0;
dst[used++] = END;
return used;
}

typedef struct {
uint8_t data[8]; /* choose a bound for your protocol */
size_t used;
bool started, escaped, discard;
} SlipRx;

/* Feed one UART byte: 1 = packet, 0 = pending, -1 = damaged/oversize. */
static int slip_feed(SlipRx *rx, uint8_t byte,
uint8_t *packet, size_t *packet_len) {
if (byte == END) {
int event = 0;
if (rx->started) {
if (rx->discard || rx->escaped) event = -1;
else if (rx->used != 0) {
memcpy(packet, rx->data, rx->used);
*packet_len = rx->used;
event = 1;
}
}
rx->started = true;
rx->used = 0;
rx->escaped = false;
rx->discard = false;
return event;
}
if (!rx->started || rx->discard) return 0;
if (rx->escaped) {
rx->escaped = false;
if (byte == ESC_END) byte = END;
else if (byte == ESC_ESC) byte = ESC;
else { rx->discard = true; return 0; }
} else if (byte == ESC) {
rx->escaped = true;
return 0;
}
if (rx->used == sizeof rx->data) {
rx->discard = true;
return 0;
}
rx->data[rx->used++] = byte;
return 0;
}

int main(void) {
const uint8_t payload[] = {0x11, END, ESC, 0x22};
const uint8_t wire_expected[] = {END, 0x11, ESC, ESC_END,
ESC, ESC_ESC, 0x22, END};
uint8_t wire[16], packet[8];
size_t packet_len = 0;
size_t wire_len = slip_encode(payload, sizeof payload, wire, sizeof wire);
assert(wire_len == sizeof wire_expected);
assert(memcmp(wire, wire_expected, wire_len) == 0);
assert(slip_encode(payload, sizeof payload, wire, 7) == 0);

SlipRx rx = {0};
assert(slip_feed(&rx, 0x55, packet, &packet_len) == 0);
for (size_t i = 0; i + 1 < wire_len; ++i)
assert(slip_feed(&rx, wire[i], packet, &packet_len) == 0);
assert(slip_feed(&rx, wire[wire_len - 1], packet, &packet_len) == 1);
assert(packet_len == sizeof payload);
assert(memcmp(packet, payload, packet_len) == 0);
printf("frame: 11 C0 DB 22\n");

const uint8_t invalid[] = {END, ESC, 0x01, END};
int event = 0;
for (size_t i = 0; i < sizeof invalid; ++i)
event = slip_feed(&rx, invalid[i], packet, &packet_len);
assert(event == -1);
printf("bad escape: dropped\n");

assert(slip_feed(&rx, END, packet, &packet_len) == 0);
for (int i = 0; i < 9; ++i)
assert(slip_feed(&rx, 0x42, packet, &packet_len) == 0);
assert(slip_feed(&rx, END, packet, &packet_len) == -1);
printf("oversize: dropped\n");
return 0;
}

GCC에서 gcc -std=c11 -Wall -Wextra -Werror slip_frame.c -o slip_frame으로 빌드한 뒤 실행했습니다. 정상 프레임의 복원, 출력 버퍼 부족, 잘못된 이스케이프, 수신 버퍼 초과를 assert로 검사합니다.

frame: 11 C0 DB 22
bad escape: dropped
oversize: dropped

펌웨어에 옮길 때

UART 인터럽트나 DMA에서 새로 받은 각 바이트를 순서대로 slip_feed에 전달하세요. 완료 이벤트 1에서만 복원 패킷을 상위 처리기로 넘기고, -1은 오류 카운터에 기록한 뒤 다음 프레임을 기다립니다. 이 예제는 단일 실행 문맥을 가정하므로 ISR과 작업이 같은 상태를 동시에 쓰지 않도록 소유권을 정해야 합니다. 실제 패킷의 최대 길이에 맞춰 버퍼를 정하고, 큰 패킷은 별도의 저장소·큐 정책을 설계하세요.

SLIP 자체에는 오류 검출, 패킷 종류, 재전송 기능이 없습니다. 데이터 무결성과 재시도 정책이 필요하다면 상위 프로토콜에서 별도로 정의해야 합니다. 이 코드는 PC에서 프레임 변환을 검증한 것이며, 특정 MCU의 UART 타이밍이나 전기적 오류를 시험한 결과는 아닙니다.

관련 글

댓글

첫 댓글을 남겨 보세요.

관리자 확인 후 게시됩니다.

처리 중입니다...

잠시만 기다려주세요.