및 Arduino–TurtleBot3 Raspberry Pi 4 연동 프로토콜 정리
1. 시리얼 통신이란?
시리얼 통신은 데이터를 한 번에 여러 비트씩 보내는 병렬 통신과 달리, 데이터를 1비트씩 순서대로 전송하는 통신 방식입니다. 구조가 단순하고 구현이 쉬워 Arduino, STM32, Raspberry Pi, 센서 모듈, 모터 드라이버, 로봇 제어 보드 등에서 많이 사용됩니다.
로봇 시스템에서는 보통 다음과 같은 상황에서 시리얼 통신을 사용합니다.
Arduino가 센서값을 읽고 Raspberry Pi로 전달할 때
Raspberry Pi가 Arduino에게 릴레이, 모터, LED, 부저 등을 제어하라고 명령할 때
상위 제어기인 ROS 노드가 하위 MCU 보드와 데이터를 주고받을 때
TurtleBot3 Raspberry Pi 4와 Arduino를 연결하는 경우에도 기본 구조는 같습니다. Arduino는 센서와 액추에이터에 가까운 하위 제어기 역할을 하고, Raspberry Pi 4는 ROS를 실행하면서 상위 판단과 데이터 처리를 담당합니다.
2. UART 기반 시리얼 통신의 기본 개념
Arduino와 Raspberry Pi 사이에서 가장 흔히 사용하는 시리얼 통신은 UART 방식입니다. UART는 별도의 클럭선을 사용하지 않고 TX, RX 신호선으로 데이터를 주고받습니다.
기본 연결은 다음과 같습니다.

주의할 점은 TX와 RX를 서로 교차 연결해야 한다는 것입니다. Arduino의 TX는 Raspberry Pi의 RX로, Arduino의 RX는 Raspberry Pi의 TX로 연결합니다. GND는 반드시 공통으로 연결해야 합니다.
USB 케이블로 Arduino와 Raspberry Pi를 연결하면 보통 /dev/ttyACM0 또는 /dev/ttyUSB0 장치로 인식됩니다.
3. 시리얼 통신에서 프로토콜이 필요한 이유
Arduino와 TurtleBot3 Raspberry Pi 4가 시리얼 통신을 할 때 실제로 오가는 데이터는 모두 바이트입니다. 사람이 보기에는 온도 25.3도, 거리 1.25m, 센서 ON/OFF 같은 의미 있는 값이지만, 통신선 위에서는 단순히 0xAA, 0x01, 0x3F 같은 1바이트 값들이 순서대로 이동합니다.
예를 들어 Arduino가 Raspberry Pi로 초음파 거리값 하나를 보낸다고 가정하겠습니다.
거리값: 1.25
이 값을 문자열로 보내면 다음처럼 전송될 수 있습니다.
'1' '.' '2' '5'
즉 ASCII 문자 기준으로 최소 4바이트가 필요합니다.
0x31 0x2E 0x32 0x35
하지만 float 값을 그대로 4바이트 바이너리로 보내면 다음과 같이 보낼 수 있습니다.
float 1.25 → 00 00 A0 3F
문자열 방식은 값의 자리수가 늘어나면 바이트 수도 늘어납니다.
1.25 → 4 bytes
123.456 → 7 bytes
-12.3456 → 8 bytes
반면 float 바이너리 방식은 항상 4바이트입니다.
float 값 1개 = 항상 4 bytes
첨부 소스는 이 바이너리 방식을 사용합니다. 특히 union을 사용해서 float, short 같은 숫자 데이터를 바이트 배열로 나누어 송수신합니다.
4. 프로토콜의 전체 구조
Arduino 또는 Sensor I/O 보드에서 Raspberry Pi로 보내는 패킷의 샘플 구조중에 하나는 다음과 같습니다.
[Start] [Packet ID] [Length] [Payload] [End]
실제 바이트 구조는 다음과 같습니다.
0xAA 0xAA 0xAA PacketID Length Payload... 0xAA 0xEE
그림으로 보면 다음과 같습니다.

각 필드의 의미는 다음과 같습니다.
| 필드 | 크기 | 의미 |
|---|---|---|
| Start1 | 1 byte | 시작 표시, 0xAA |
| Start2 | 1 byte | 시작 표시, 0xAA |
| Start3 | 1 byte | 시작 표시, 0xAA |
| Packet ID | 1 byte | 센서 데이터 종류 |
| Length | 1 byte | Payload 길이 |
| Payload | 가변 | 실제 센서값 |
| End1 | 1 byte | 종료 표시, 0xAA |
| End2 | 1 byte | 종료 표시, 0xEE |
5. 수신 방식의 동작 원리
Raspberry Pi 쪽 ROS 노드는 시리얼 포트를 열고 별도 수신 스레드를 실행합니다. 수신 스레드는 반복적으로 시리얼 버퍼에 데이터가 있는지 확인합니다.
전체 흐름은 다음과 같습니다.

시리얼 통신에서는 패킷이 항상 한 번에 통째로 들어온다고 보장할 수 없습니다. 예를 들어 30바이트 패킷이 다음처럼 나누어 들어올 수 있습니다.
첫 번째 read() : 8 bytes
두 번째 read() : 12 bytes
세 번째 read() : 10 bytes
그래서 수신 코드는 한 번 읽은 데이터만 보고 판단하지 않고, recvBuffer에 계속 누적하면서 시작과 끝을 찾습니다.
6. 프로토콜 예시: 거리 센서 패킷
거리 센서 패킷 DISTDATA는 IR 센서 4개와 초음파 센서 4개의 값을 포함합니다.
Payload 구조는 다음과 같습니다.
IR Sensor 0 : 1 byte
IR Sensor 1 : 1 byte
IR Sensor 2 : 1 byte
IR Sensor 3 : 1 byte
Ultrasonic Sensor0 : float, 4 bytes
Ultrasonic Sensor1 : float, 4 bytes
Ultrasonic Sensor2 : float, 4 bytes
Ultrasonic Sensor3 : float, 4 bytes
Payload 크기는 다음과 같습니다.
IR 4개 = 1 × 4 = 4 bytes
초음파 4개 = 4 × 4 = 16 bytes
총 Payload = 20 bytes
패킷 그림은 다음과 같습니다.

여기서 0x14는 10진수 20입니다. 즉 Payload 길이가 20바이트라는 뜻입니다.
실제 바이트 인덱스는 다음과 같이 해석됩니다.
recvBuffer[0] = 0xAA
recvBuffer[1] = 0xAA
recvBuffer[2] = 0xAA
recvBuffer[3] = DISTDATA
recvBuffer[4] = 20
recvBuffer[5] = IR0
recvBuffer[6] = IR1
recvBuffer[7] = IR2
recvBuffer[8] = IR3
recvBuffer[9] = US0 byte0
recvBuffer[10] = US0 byte1
recvBuffer[11] = US0 byte2
recvBuffer[12] = US0 byte3
recvBuffer[13] = US1 byte0
recvBuffer[14] = US1 byte1
recvBuffer[15] = US1 byte2
recvBuffer[16] = US1 byte3
recvBuffer[17] = US2 byte0
recvBuffer[18] = US2 byte1
recvBuffer[19] = US2 byte2
recvBuffer[20] = US2 byte3
recvBuffer[21] = US3 byte0
recvBuffer[22] = US3 byte1
recvBuffer[23] = US3 byte2
recvBuffer[24] = US3 byte3
recvBuffer[25] = 0xAA
recvBuffer[26] = 0xEE
7. union을 이용한 float 송수신 원리
첨부 소스에서 중요한 부분은 float 값을 union으로 바이트 단위로 다루는 방식입니다.
일반적인 union 구조는 다음과 같이 만들 수 있습니다.
typedef union
{
float fval;
uint8_t cval[4];
} FloatByte;
이 union은 같은 메모리 공간을 두 가지 방식으로 바라봅니다.

즉, 다음 두 표현은 같은 메모리를 공유합니다.
fval : float 값 하나
cval[4] : float 값을 구성하는 4개의 byte
예를 들어 float 값 1.25가 있다고 하면 내부 바이트는 일반적인 little-endian 시스템에서 다음과 같이 저장됩니다.

Arduino에서 보낼 때는 다음처럼 보낼 수 있습니다.
FloatByte data;
data.fval = 1.25f;
Serial.write(data.cval[0]);
Serial.write(data.cval[1]);
Serial.write(data.cval[2]);
Serial.write(data.cval[3]);
Raspberry Pi에서 받을 때는 반대로 4개의 바이트를 넣고 float로 읽습니다.
FloatByte data;
data.cval[0] = recvBuffer[9];
data.cval[1] = recvBuffer[10];
data.cval[2] = recvBuffer[11];
data.cval[3] = recvBuffer[12];
float distance = data.fval;
그림으로 보면 다음과 같습니다.

8. union 방식이 송수신 바이트를 줄이는 이유
정확히 말하면 union 자체가 float의 크기를 줄이는 것은 아닙니다. float는 원래 4바이트입니다. union은 이 4바이트를 쉽게 쪼개고 다시 조립하게 해주는 방법입니다.
바이트 수를 줄이는 핵심은 “문자열로 보내지 않고 바이너리로 보낸다”는 점입니다.
예를 들어 온도값 123.456을 보낸다고 하겠습니다.
문자열 방식:
"123.456"
전송 바이트는 다음과 같습니다.
'1' '2' '3' '.' '4' '5' '6'
총 7바이트입니다.
바이너리 float 방식:
float 123.456
전송 바이트는 항상 4바이트입니다.
79 E9 F6 42
비교하면 다음과 같습니다.
| 값 | 문자열 전송 | float 바이너리 전송 |
1.2 | 3 bytes | 4 bytes |
12.34 | 5 bytes | 4 bytes |
123.456 | 7 bytes | 4 bytes |
-123.4567 | 9 bytes | 4 bytes |
값이 짧을 때는 문자열이 더 작을 수도 있지만, 센서값이 많아지면 바이너리 방식이 훨씬 안정적이고 일정합니다. 특히 센서 10개, 20개를 계속 보내는 로봇 시스템에서는 바이너리 방식이 유리합니다.
예를 들어 가스 센서 데이터는 float 13개입니다.
문자열 방식으로 각 값이 평균 7바이트라고 하면 다음과 같습니다.
13개 × 7 bytes = 91 bytes
쉼표 12개 = 12 bytes
총합 = 약 103 bytes
바이너리 float 방식은 다음과 같습니다.
13개 × 4 bytes = 52 bytes
즉, 절반 가까이 줄어듭니다.
9. short 데이터도 union으로 처리하는 방식
첨부 소스에서는 화재 센서값을 short 값으로 처리합니다.
일반적인 short union은 다음과 같습니다.
typedef union
{
int16_t sval;
uint8_t cval[2];
} ShortByte;
메모리 구조는 다음과 같습니다.

예를 들어 short 값 300은 16진수로 0x012C입니다. little-endian에서는 다음 순서로 저장됩니다.

송신 측 Arduino:
ShortByte fire;
fire.sval = 300;
Serial.write(fire.cval[0]);
Serial.write(fire.cval[1]);
수신 측 Raspberry Pi:
ShortByte fire;
fire.cval[0] = recvBuffer[5];
fire.cval[1] = recvBuffer[6];
int16_t fire_value = fire.sval;
10. union 방식 사용 시 반드시 주의할 점
union 방식은 빠르고 간단하지만, 몇 가지 주의점이 있습니다.
첫째, 송신 측과 수신 측의 자료형 크기가 같아야 합니다.
float = 4 bytes
int16_t = 2 bytes
uint8_t = 1 byte
Arduino와 Raspberry Pi에서 float가 둘 다 4바이트인지 확인해야 합니다. 일반적인 Arduino UNO, Mega, STM32, ESP32, Raspberry Pi 환경에서는 float가 보통 4바이트입니다.
둘째, 엔디언이 같아야 합니다.
대부분의 Arduino 계열과 Raspberry Pi는 little-endian입니다. 따라서 아래 순서가 보통 맞습니다.
byte0 → 최하위 바이트
byte1
byte2
byte3 → 최상위 바이트
하지만 다른 MCU나 통신 상대와 연결할 경우에는 문제가 생길 수 있습니다. 가장 안전한 방법은 프로토콜 문서에 바이트 순서를 명확히 적는 것입니다.
권장 표기는 다음과 같습니다.
float32 전송 순서: little-endian
byte0 = LSB
byte1
byte2
byte3 = MSB
셋째, 구조체를 통째로 보내는 방식은 피하는 것이 좋습니다.
예를 들어 다음처럼 구조체를 그대로 보내는 방식은 위험합니다.
Serial.write((uint8_t *)&packet, sizeof(packet));
구조체에는 컴파일러가 자동으로 padding byte를 넣을 수 있기 때문입니다. 그래서 실무에서는 바이트 배열에 직접 넣는 방식이 더 안전합니다.
11. ROS2 보드의 firmware에서 사용할 프로토콜 설계 방향
프로토콜의 안정성을 높이려면 다음 필드를 추가하거나 강화하면 됩니다.
Start 3 bytes
Packet ID 1 byte
Length 1 byte
Sequence 1 byte
Payload N bytes
Checksum 1 byte
End 2 bytes
패킷의 구조는 다음과 같습니다.
0xAA 0xAA 0xAA PacketID Length Sequence Payload... Checksum 0xAA 0xEE
그림으로 보면 다음과 같습니다.

각 필드 의미는 다음과 같습니다.
| 필드 | 크기 | 설명 |
| Start | 3 bytes | 0xAA 0xAA 0xAA |
| Packet ID | 1 byte | 센서 또는 명령 종류 |
| Length | 1 byte | Payload 길이 |
| Sequence | 1 byte | 패킷 번호 |
| Payload | N bytes | 실제 데이터 |
| Checksum | 1 byte | 오류 검출 |
| End | 2 bytes | 0xAA 0xEE |
1) Checksum 계산
가장 단순한 checksum은 1바이트 합산 방식입니다.
Checksum 계산 범위는 다음으로 정합니다.
Packet ID + Length + Sequence + Payload 전체 합
계산 결과의 하위 1바이트만 사용합니다.
Checksum = (PacketID + Length + Sequence + Payload bytes) & 0xFF
그림으로 보면 다음과 같습니다.

Arduino 송신 측 예시는 다음과 같습니다.
uint8_t calcChecksum(uint8_t packet_id,
uint8_t length,
uint8_t sequence,
uint8_t *payload)
{
uint16_t sum = 0;
sum += packet_id;
sum += length;
sum += sequence;
for (int i = 0; i < length; i++)
{
sum += payload[i];
}
return (uint8_t)(sum & 0xFF);
}
수신 측에서는 같은 방식으로 checksum을 다시 계산합니다.
수신 checksum == 계산 checksum → 정상 패킷
수신 checksum != 계산 checksum → 오류 패킷, 폐기
2) Length 기반 파싱
기존 방식은 종료 바이트를 계속 찾습니다. 개선 방식에서는 Length를 기준으로 전체 패킷 길이를 계산합니다.
개선 패킷의 전체 길이는 다음과 같습니다.
전체 길이 = Start 3 + PacketID 1 + Length 1 + Sequence 1 + Payload Length + Checksum 1 + End 2
즉,
Total Length = 9 + Payload Length
예를 들어 Payload가 20바이트이면 전체 패킷은 다음과 같습니다.
Total Length = 9 + 20 = 29 bytes
수신 코드는 다음 순서로 동작하는 것이 좋습니다.

이 방식은 Payload 내부에 0xAA 0xEE가 들어와도 문제가 없습니다. 왜냐하면 종료 바이트를 아무 위치에서나 찾지 않고, Length로 계산된 정확한 위치에서만 확인하기 때문입니다.
3) 거리 센서 패킷 예시
거리 센서 데이터는 Payload 20바이트입니다.
IR 4개 = 4 bytes
US float 4개 = 16 bytes
Payload = 20 bytes
패킷은 다음과 같습니다.

예를 들어 다음 값이 있다고 하겠습니다.
IR0 = 10
IR1 = 20
IR2 = 30
IR3 = 40
US0 = 1.25
US1 = 2.50
US2 = 3.75
US3 = 4.00
float 값을 little-endian으로 바꾸면 예시는 다음과 같습니다.
1.25 → 00 00 A0 3F
2.50 → 00 00 20 40
3.75 → 00 00 70 40
4.00 → 00 00 80 40
Payload는 다음과 같습니다.
0A 14 1E 28 00 00 A0 3F 00 00 20 40 00 00 70 40 00 00 80 40
전체 패킷 예시는 다음과 같습니다.
AA AA AA DISTDATA 14 01
0A 14 1E 28 00 00 A0 3F 00 00 20 40 00 00 70 40 00 00 80 40
CHK
AA EE
여기서 01은 Sequence 번호입니다. 다음 패킷은 02, 그 다음은 03처럼 증가시킬 수 있습니다.
Sequence는 필수는 아니지만 로봇 시스템에서는 넣는 것이 좋습니다.
Sequence가 있으면 다음 상황을 확인할 수 있습니다.
패킷 누락 여부
패킷 순서 꼬임 여부
동일 패킷 반복 수신 여부
예를 들어 Raspberry Pi가 다음과 같은 순서로 패킷을 받아야 한다고 하겠습니다.
Seq 10
Seq 11
Seq 12
Seq 13
그런데 실제 수신이 다음처럼 되면,
Seq 10
Seq 11
Seq 13
Seq 12가 누락되었음을 알 수 있습니다.
Sequence는 1바이트만 써도 충분합니다.
0 → 1 → 2 → ... → 254 → 255 → 0
4) PMSDATA 구조
PSMDATA 패킷은 PM1.0, PM2.5, PM10을 각각 float 4바이트로 분리합니다.
Payload 구조는 다음과 같습니다.
PM1.0 : 4 bytes
PM2.5 : 4 bytes
PM10 : 4 bytes
총 Payload는 12바이트입니다.
Payload Length = 12
패킷 구조는 다음과 같습니다.

수신 인덱스는 다음처럼 정리하는 것이 맞습니다.
payload[0] ~ payload[3] : PM1.0
payload[4] ~ payload[7] : PM2.5
payload[8] ~ payload[11] : PM10
기존 recvBuffer 기준으로는 Sequence가 추가되었으므로 Payload 시작 위치를 고정 인덱스로 직접 쓰기보다 payload_start를 기준으로 계산하는 것이 좋습니다.
int payload_start = 6;
float pm1 = bytesToFloat(&recvBuffer[payload_start + 0]);
float pm25 = bytesToFloat(&recvBuffer[payload_start + 4]);
float pm10 = bytesToFloat(&recvBuffer[payload_start + 8]);
5) GASDATA 구조
가스 센서 데이터는 float 13개입니다.
LPG
CO
smoke
alcohol
CO2
toluen
NH4
aceton
H2
C2H5OH
C4H10
humidity
temperature
Payload 길이는 다음과 같습니다.
13 × 4 bytes = 52 bytes
개선 패킷 구조는 다음과 같습니다.

0x34는 10진수 52입니다.
Payload 내부 구조는 다음과 같습니다.
payload[0] ~ [3] : LPG
payload[4] ~ [7] : CO
payload[8] ~ [11] : smoke
payload[12] ~ [15] : alcohol
payload[16] ~ [19] : CO2
payload[20] ~ [23] : toluen
payload[24] ~ [27] : NH4
payload[28] ~ [31] : aceton
payload[32] ~ [35] : H2
payload[36] ~ [39] : C2H5OH
payload[40] ~ [43] : C4H10
payload[44] ~ [47] : humidity
payload[48] ~ [51] : temperature
6) SETRUNSENSOR 명령 패킷
기존 Raspberry Pi → Arduino 명령 패킷은 0x3B로 끝납니다.
기존 구조:
AA AA AA SETRUNSENSOR 02 sensor_num state 3B
센서 데이터 패킷과 같은 형식으로 통일하는 것이 좋습니다.
구조:
AA AA AA SETRUNSENSOR 02 Seq sensor_num state Checksum AA EE
그림은 다음과 같습니다.

이렇게 하면 Arduino 쪽 수신 파서와 Raspberry Pi 쪽 수신 파서를 거의 같은 구조로 만들 수 있습니다.
7) 권장 Packet ID 정리
실제 숫자는 프로젝트 헤더 파일에서 확정해야 합니다. 예시는 다음처럼 정리할 수 있습니다.
#define DISTDATA 0x10
#define ETCDATA 0x11
#define NCTSDATA 0x12
#define PMSDATA 0x13
#define GASDATA 0x14
#define SETRUNSENSOR 0x20
중요한 원칙은 Arduino와 Raspberry Pi가 같은 값을 사용해야 한다는 점입니다.
권장 방식은 양쪽 코드에 같은 헤더 또는 같은 표를 기준으로 정의하는 것입니다.
Arduino 코드의 DISTDATA 값 == Raspberry Pi 코드의 DISTDATA 값
Arduino 코드의 GASDATA 값 == Raspberry Pi 코드의 GASDATA 값
8) 프로토콜 최종 요약
최종 권장 프로토콜은 다음과 같습니다.
Start : 0xAA 0xAA 0xAA
Packet ID : 1 byte
Length : 1 byte
Sequence : 1 byte
Payload : Length bytes
Checksum : 1 byte
End : 0xAA 0xEE
전체 그림은 다음과 같습니다.

전체 패킷 길이는 다음과 같습니다.
Total Length = 3 + 1 + 1 + 1 + Length + 1 + 2
Total Length = 9 + Length
12. 프로토콜 Arduino 송신 예시
float 값을 바이트로 변환하기 위한 union은 다음과 같습니다.
typedef union
{
float fval;
uint8_t b[4];
} FloatByte;
Payload에 float를 넣는 함수는 다음과 같습니다.
void putFloat(uint8_t *payload, int index, float value)
{
FloatByte fb;
fb.fval = value;
payload[index + 0] = fb.b[0];
payload[index + 1] = fb.b[1];
payload[index + 2] = fb.b[2];
payload[index + 3] = fb.b[3];
}
Checksum 함수는 다음과 같습니다.
uint8_t calcChecksum(uint8_t packet_id,
uint8_t length,
uint8_t sequence,
uint8_t *payload)
{
uint16_t sum = 0;
sum += packet_id;
sum += length;
sum += sequence;
for (int i = 0; i < length; i++)
{
sum += payload[i];
}
return (uint8_t)(sum & 0xFF);
}
패킷 송신 함수는 다음과 같습니다.
void sendPacket(uint8_t packet_id,
uint8_t sequence,
uint8_t *payload,
uint8_t length)
{
uint8_t checksum = calcChecksum(packet_id, length, sequence, payload);
Serial.write(0xAA);
Serial.write(0xAA);
Serial.write(0xAA);
Serial.write(packet_id);
Serial.write(length);
Serial.write(sequence);
for (int i = 0; i < length; i++)
{
Serial.write(payload[i]);
}
Serial.write(checksum);
Serial.write(0xAA);
Serial.write(0xEE);
}
거리 센서 패킷 송신 예시는 다음과 같습니다.
void sendDistData(uint8_t sequence,
uint8_t ir0,
uint8_t ir1,
uint8_t ir2,
uint8_t ir3,
float us0,
float us1,
float us2,
float us3)
{
uint8_t payload[20];
payload[0] = ir0;
payload[1] = ir1;
payload[2] = ir2;
payload[3] = ir3;
putFloat(payload, 4, us0);
putFloat(payload, 8, us1);
putFloat(payload, 12, us2);
putFloat(payload, 16, us3);
sendPacket(DISTDATA, sequence, payload, 20);
}
13. Arduino 송신 프로토콜을 Raspberry Pi에서 수신하여 해석
Raspberry Pi 쪽에서는 Payload에서 float 값을 다시 복원합니다.
typedef union
{
float fval;
uint8_t b[4];
} FloatByte;
float getFloat(uint8_t *payload, int index)
{
FloatByte fb;
fb.b[0] = payload[index + 0];
fb.b[1] = payload[index + 1];
fb.b[2] = payload[index + 2];
fb.b[3] = payload[index + 3];
return fb.fval;
}
거리 센서 데이터 해석 예시는 다음과 같습니다.
void parseDistData(uint8_t *payload, uint8_t length)
{
if (length != 20)
return;
uint8_t ir0 = payload[0];
uint8_t ir1 = payload[1];
uint8_t ir2 = payload[2];
uint8_t ir3 = payload[3];
float us0 = getFloat(payload, 4);
float us1 = getFloat(payload, 8);
float us2 = getFloat(payload, 12);
float us3 = getFloat(payload, 16);
// ROS 메시지에 넣고 publish
}
이렇게 하면 recvBuffer[9], recvBuffer[13] 같은 고정 인덱스를 여기저기 직접 쓰는 것보다 실수가 줄어듭니다.
14. 수신 상태 머신
시리얼 수신은 상태 머신으로 작성하는 것이 가장 안정적입니다.
상태는 다음처럼 나눌 수 있습니다.
WAIT_START_1
WAIT_START_2
WAIT_START_3
READ_PACKET_ID
READ_LENGTH
READ_SEQUENCE
READ_PAYLOAD
READ_CHECKSUM
READ_END_1
READ_END_2
PROCESS_PACKET
흐름은 다음과 같습니다.

이 방식의 장점은 다음과 같습니다.
패킷 중간 손상 시 복구가 쉬움
Payload 안에 0xAA 0xEE가 있어도 문제 없음
Length 기반으로 정확한 패킷 크기 판단 가능
Buffer overflow 방어 가능
15. 프로토콜 적용 시 데이터 크기 비교
거리 센서 데이터를 예로 들면 다음과 같습니다.
기존 방식:
Start 3
Packet ID 1
Length 1
Payload 20
End 2
총 27 bytes
개선 방식:
Start 3
Packet ID 1
Length 1
Sequence 1
Payload 20
Checksum 1
End 2
총 29 bytes
2바이트가 늘어납니다. 하지만 이 2바이트로 다음 기능이 생깁니다.
패킷 누락 확인
통신 오류 검출
Length 검증
안정적인 재동기화
로봇에서는 2바이트를 아끼는 것보다 잘못된 센서값으로 오동작하지 않는 것이 훨씬 중요합니다.
16. union 방식과 개선 프로토콜을 함께 사용할 때의 최종 구조
최종 구조는 다음처럼 이해하면 됩니다.

그림으로 보면 다음과 같습니다.
