아두이노 시리얼 버퍼 크기 늘리기 안내 이미지

PC에서 아두이노로 LED 색상 데이터나 긴 명령 문자열을 한 번에 보내다 보면, 앞부분만 들어오고 뒤가 잘리는 일이 생깁니다. 대부분 아두이노 쪽 시리얼 수신 버퍼가 넘쳐서 생기는 현상입니다. UART로 들어온 바이트는 인터럽트가 받아서 링 버퍼에 쌓아 두고, 스케치는 Serial.read()로 거기서 꺼내 갑니다. 꺼내 가는 속도보다 들어오는 속도가 빠르면 버퍼가 가득 차고, 그 뒤로 들어온 바이트는 그냥 버려집니다.

광고

기본 버퍼는 64바이트

UNO처럼 ATmega328P를 쓰는 보드는 송신·수신 버퍼가 각각 64바이트입니다. 이 값은 아두이노 코어의 HardwareSerial.h에 SERIAL_TX_BUFFER_SIZE, SERIAL_RX_BUFFER_SIZE로 정의되어 있습니다. RAM이 1KB도 안 되는 칩에서는 16바이트로 더 작게 잡힙니다.

115200bps면 1바이트에 대략 87µs 정도라서, 64바이트가 차는 데 6ms도 안 걸립니다. loop() 안에서 delay()를 쓰거나 LED 제어처럼 시간이 걸리는 작업을 하는 동안 데이터가 몰려 들어오면 금방 넘칩니다.

HardwareSerial.h 수정하기

버퍼 자체를 키우려면 코어 파일을 고쳐야 합니다. UNO 기준으로 해당 부분을 아래처럼 256으로 바꿉니다. 다른 보드는 그 보드용 코어 폴더의 HardwareSerial.h를 찾아야 합니다.

#if !defined(SERIAL_TX_BUFFER_SIZE)
#if ((RAMEND - RAMSTART) < 1023)
#define SERIAL_TX_BUFFER_SIZE 16
#else
#define SERIAL_TX_BUFFER_SIZE 256  // 기존 64에서 256으로 버퍼 크기를 늘림
#endif
#endif
#if !defined(SERIAL_RX_BUFFER_SIZE)
#if ((RAMEND - RAMSTART) < 1023)
#define SERIAL_RX_BUFFER_SIZE 16
#else
#define SERIAL_RX_BUFFER_SIZE 256  // 기존 64에서 256으로 버퍼 크기를 늘림
#endif
#endif

이 방법에는 알고 있어야 할 점이 몇 가지 있습니다. 코어 파일을 직접 고치는 것이라 그 PC에서 빌드하는 모든 스케치에 적용되고, 보드 패키지를 업데이트하면 원래 값으로 덮어써집니다. 다른 PC에서 같은 스케치를 빌드하면 당연히 64바이트로 돌아가고요. 그리고 버퍼는 SRAM에 잡히니, UNO처럼 SRAM이 2KB뿐인 보드에서 송수신을 각각 256으로 늘리면 그만큼 스케치가 쓸 메모리가 줄어듭니다. 컴파일 후 "전역 변수가 사용하는 메모리" 수치를 꼭 확인하세요.

스케치 쪽에서 프레임 단위로 모으기

버퍼를 키우는 것보다 먼저 해 볼 것은, 들어오는 바이트를 바로바로 꺼내서 스케치의 배열에 모아 두는 방식입니다. 시리얼 버퍼는 "잠깐 쌓아 두는 곳"으로만 쓰고, 실제 데이터 묶음은 내 배열에서 완성시키는 거죠. 아래는 LED 256개의 RGB 값(768바이트)을 받아 처리하는 예제입니다.

#define LED_COUNT 256
#define LED_DATA_SIZE (LED_COUNT * 3)

uint8_t buffer[LED_DATA_SIZE];
uint16_t bufferLength = 0;

void setup() {
  Serial.begin(115200);  // RS232 통신 속도 설정 (수신용)
  // 기타 초기화 작업
}

void loop() {
  // 데이터를 받아서 버퍼에 저장
  while (Serial.available() > 0) {
    buffer[bufferLength++] = Serial.read();

    // LED 데이터 사이즈에 도달하면 LED 데이터 처리
    if (bufferLength == LED_DATA_SIZE) {
      processData(buffer, bufferLength);  // LED 데이터 처리 함수 호출
      bufferLength = 0;  // 버퍼 초기화
    }
  }

  // 추가적인 조건에 따라 나머지 데이터 처리
  if (bufferLength > 0) {
    processData(buffer, bufferLength);  // 나머지 데이터 처리 함수 호출
    bufferLength = 0;  // 버퍼 초기화
  }
}

void processData(uint8_t *data, uint16_t length) {
  // LED 데이터 처리 로직 예시
  for (int i = 0; i < LED_COUNT; i++) {
    int startIndex = i * 3;
    uint8_t r = data[startIndex];
    uint8_t g = data[startIndex + 1];
    uint8_t b = data[startIndex + 2];

    // 여기에 LED 제어 로직 추가 (예: WS2812 등의 LED 제어)
    // 예시: analogWrite(pin, value); 또는 digitalWrite(pin, HIGH/LOW); 등

    // 이 예제에서는 Serial Monitor를 통해 데이터 출력
    Serial.print("LED ");
    Serial.print(i);
    Serial.print(": R=");
    Serial.print(r);
    Serial.print(", G=");
    Serial.print(g);
    Serial.print(", B=");
    Serial.println(b);
  }
}

while 루프에서 들어온 바이트를 buffer에 차곡차곡 넣다가 768바이트가 채워지면 processData()로 넘깁니다. 이렇게 하면 시리얼 버퍼가 64바이트든 256바이트든 상관없이 스케치 쪽 배열에서 한 프레임이 완성됩니다. 다만 768바이트 배열도 SRAM에 올라가므로, UNO에서 위의 256바이트 버퍼 확장까지 같이 하면 남는 메모리가 꽤 빠듯해집니다.

주의: "나머지 데이터 처리" 부분

예제의 loop() 아래쪽 if (bufferLength > 0) 블록은 조심해서 써야 합니다. Serial.available()이 잠깐이라도 0이 되면, 즉 768바이트가 다 들어오기 전 중간에 바이트 사이 틈이 생기면 그 즉시 반쪽짜리 데이터로 processData()를 호출하고 버퍼를 비워 버립니다. 게다가 processData()는 length를 보지 않고 항상 256개 LED 분량을 읽기 때문에, 아직 안 채워진 부분은 이전 데이터나 쓰레기값이 섞여 나옵니다. 115200bps로 받으면 바이트 사이 틈이 루프 한 바퀴보다 길어지는 경우가 흔해서 실제로는 이 블록 때문에 프레임이 계속 쪼개질 수 있습니다.

프레임이 다 찼을 때만 처리하려면 그 블록을 빼고, 대신 일정 시간 동안 데이터가 안 들어오면 버퍼를 버리도록 바꾸는 편이 안전합니다. 아래는 그렇게 고쳐 본 loop()입니다(원문 코드에서 수정한 부분).

unsigned long lastRecv = 0;

void loop() {
  while (Serial.available() > 0) {
    buffer[bufferLength++] = Serial.read();
    lastRecv = millis();

    if (bufferLength == LED_DATA_SIZE) {
      processData(buffer, bufferLength);
      bufferLength = 0;
    }
  }

  // 100ms 동안 추가 데이터가 없으면 미완성 프레임은 버림
  if (bufferLength > 0 && millis() - lastRecv > 100) {
    bufferLength = 0;
  }
}

그리고 예제처럼 processData() 안에서 Serial.print()로 768줄 가까이 출력하면, 그 출력이 끝날 때까지 다음 데이터를 꺼내 가지 못해 수신 버퍼가 다시 넘칩니다. 확인용 출력은 테스트할 때만 쓰고, 실제로는 WS2812 같은 LED 라이브러리 호출로 바꿔야 합니다.

그래도 부족할 때

보내는 쪽에서 데이터를 작은 패킷으로 나누고, 패킷마다 순번을 붙여 아두이노가 받았다는 응답을 보낸 뒤에 다음 패킷을 보내는 방식으로 바꾸면 버퍼 크기와 무관하게 큰 데이터를 옮길 수 있습니다. 데이터 자체가 커서 SRAM에 다 담을 수 없다면 SD 카드 같은 외부 저장장치에 쓰면서 처리하거나, 처음부터 RAM이 넉넉한 보드(Mega, ESP32 등)로 옮기는 게 현실적입니다.