Stream 클래스

오늘은 Arduino 클래스 중 하나인 Stream C++ 클래스를 분석해본다.
해당 클래스가 중요한 이유는 데이터 입출력 처리의 기본 클래스로써 WiFi 부터 Ethernet, UART, I2C, SPI 등등 각종 통신에서 Stream 클래스를 상속하여 사용하고 있기 때문이다. (Stream 클래스를 기반으로 인터페이스 된다.)
코드의 내용은 오픈소스로써 깃허브 주소에서 확인 할 수 있다.
https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Stream.cpp
arduino-esp32/cores/esp32/Stream.cpp at master · espressif/arduino-esp32
Arduino core for the ESP32. Contribute to espressif/arduino-esp32 development by creating an account on GitHub.
github.com
- Stream은 연속적인 데이터의 흐름을 의미하는 개념으로 시리얼 통신 같은 입출력(I/O)에서 데이터를 다루는 방식
- Arduino의 대부분의 통신은 Stream 클래스를 기반으로 데이터를 다룬다.
- Stream 클래스를 상속받아 Serial, WiFiClient, SoftwareSerial 등의 구체적인 클래스들이 구현된다.
클래스 선언부 (h)
/*
Stream.h - base class for character-based streams.
Copyright (c) 2010 David A. Mellis. All right reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
parsing functions based on TextFinder library by Michael Margolis
*/
#pragma once
#include <inttypes.h>
#include "Print.h"
// compatibility macros for testing
/*
#define getInt() parseInt()
#define getInt(ignore) parseInt(ignore)
#define getFloat() parseFloat()
#define getFloat(ignore) parseFloat(ignore)
#define getString( pre_string, post_string, buffer, length)
readBytesBetween( pre_string, terminator, buffer, length)
*/
// This enumeration provides the lookahead options for parseInt(), parseFloat()
// The rules set out here are used until either the first valid character is found
// or a time out occurs due to lack of input.
enum LookaheadMode {
SKIP_ALL, // All invalid characters are ignored.
SKIP_NONE, // Nothing is skipped, and the stream is not touched unless the first waiting character is valid.
SKIP_WHITESPACE // Only tabs, spaces, line feeds & carriage returns are skipped.
};
#define NO_IGNORE_CHAR '\x01' // a char not found in a valid ASCII numeric field
class Stream : public Print {
protected:
unsigned long _timeout; // number of milliseconds to wait for the next char before aborting timed read
unsigned long _startMillis; // used for timeout measurement
int timedRead(); // private method to read stream with timeout
int timedPeek(); // private method to peek stream with timeout
int peekNextDigit(LookaheadMode lookahead, bool detectDecimal); // returns the next numeric digit in the stream or -1 if timeout
public:
virtual int available() = 0;
virtual int read() = 0;
virtual int peek() = 0;
Stream() {
_timeout = 1000;
}
// parsing methods
void setTimeout(unsigned long timeout); // sets maximum milliseconds to wait for stream data, default is 1 second
unsigned long getTimeout(void) {
return _timeout;
}
bool find(const char *target); // reads data from the stream until the target string is found
bool find(const uint8_t *target) {
return find((const char *)target);
}
// returns true if target string is found, false if timed out (see setTimeout)
bool find(const char *target, size_t length); // reads data from the stream until the target string of given length is found
bool find(const uint8_t *target, size_t length) {
return find((const char *)target, length);
}
// returns true if target string is found, false if timed out
bool find(char target) {
return find(&target, 1);
}
bool findUntil(const char *target, const char *terminator); // as find but search ends if the terminator string is found
bool findUntil(const uint8_t *target, const char *terminator) {
return findUntil((const char *)target, terminator);
}
bool findUntil(const char *target, size_t targetLen, const char *terminate, size_t termLen); // as above but search ends if the terminate string is found
bool findUntil(const uint8_t *target, size_t targetLen, const char *terminate, size_t termLen) {
return findUntil((const char *)target, targetLen, terminate, termLen);
}
long parseInt(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR);
// returns the first valid (long) integer value from the current position.
// lookahead determines how parseInt looks ahead in the stream.
// See LookaheadMode enumeration at the top of the file.
// Lookahead is terminated by the first character that is not a valid part of an integer.
// Once parsing commences, 'ignore' will be skipped in the stream.
float parseFloat(LookaheadMode lookahead = SKIP_ALL, char ignore = NO_IGNORE_CHAR);
// float version of parseInt
virtual size_t readBytes(char *buffer, size_t length); // read chars from stream into buffer
virtual size_t readBytes(uint8_t *buffer, size_t length) {
return readBytes((char *)buffer, length);
}
// terminates if length characters have been read or timeout (see setTimeout)
// returns the number of characters placed in the buffer (0 means no valid data found)
size_t readBytesUntil(char terminator, char *buffer, size_t length); // as readBytes with terminator character
size_t readBytesUntil(char terminator, uint8_t *buffer, size_t length) {
return readBytesUntil(terminator, (char *)buffer, length);
}
// terminates if length characters have been read, timeout, or if the terminator character detected
// returns the number of characters placed in the buffer (0 means no valid data found)
// Arduino String functions to be added here
virtual String readString();
String readStringUntil(char terminator);
protected:
long parseInt(char ignore) {
return parseInt(SKIP_ALL, ignore);
}
float parseFloat(char ignore) {
return parseFloat(SKIP_ALL, ignore);
}
// These overload exists for compatibility with any class that has derived
// Stream and used parseFloat/Int with a custom ignore character. To keep
// the public API simple, these overload remains protected.
struct MultiTarget {
const char *str; // string you're searching for
size_t len; // length of string you're searching for
size_t index; // index used by the search routine.
};
// This allows you to search for an arbitrary number of strings.
// Returns index of the target that is found first or -1 if timeout occurs.
int findMulti(struct MultiTarget *targets, int tCount);
};
#undef NO_IGNORE_CHAR
1. 주요 멤버 변수
protected:
unsigned long _timeout; // 데이터 수신을 기다리는 최대 시간 (기본값 1000ms)
unsigned long _startMillis; // 타임아웃 측정을 위한 시작 시간
- _timeout: 데이터를 기다리는 최대 시간을 설정 (기본값: 1초).
- _startMillis: 타임아웃 검출을 위한 타이머 값.
2. 주요 가상 함수 (순수 가상 함수)
public:
virtual int available() = 0;
virtual int read() = 0;
virtual int peek() = 0;
- available(), read(), peek() 등의 기본적인 입출력 메서드는 순수 가상 함수(pure virtual function)로 선언되어 있어, 하위 클래스에서 반드시 구현해야 함.
- virtual 키워드는 C++에서 함수의 동작을 변경(오버라이딩) 할 수 있도록 허용하는 역할.
- virtual이 선언된 함수는 기본 클래스의 포인터나 참조를 사용하여 호출할 때는 실제 파생 클래스의 구현이 실행됨
- 이를 동적 바인딩 또는 런타임 다형성이라고 함
3. 내부적으로 사용되는 메서드
protected:
int timedRead(); // 타임아웃을 고려한 데이터 읽기
int timedPeek(); // 타임아웃을 고려한 데이터 미리보기(peek)
int peekNextDigit(LookaheadMode lookahead, bool detectDecimal);
- timedRead(), timedPeek()
- 타임아웃을 고려하여 데이터를 읽거나 미리보기를 수행.
- 타임아웃 발생 시 -1을 반환하여 데이터 없음 상태를 처리.
- peekNextDigit()
- 숫자 데이터를 분석할 때 다음 숫자를 읽어오는 함수.
- LookaheadMode를 사용하여 검색 방식을 조정.
클래스 구현부 (CPP)
1. timeRead()
int Stream::timedRead() {
int c;
_startMillis = millis();
do {
c = read(); // 실제 데이터 읽기
if (c >= 0) {
return c; // 정상적으로 데이터를 읽으면 반환
}
} while (millis() - _startMillis < _timeout);
return -1; // 시간 초과 시 -1 반환
}
- 데이터를 읽되, _timeout 내에서만 대기.
- read() 함수는 가상 함수이므로 **하위 클래스(Serial, WiFiClient 등)**에서 구현됨.
- 시간 내 데이터를 받지 못하면 -1을 반환.
2. timedPeek()
int Stream::timedPeek() {
int c;
_startMillis = millis();
do {
c = peek(); // peek()로 데이터를 확인
if (c >= 0) {
return c;
}
} while (millis() - _startMillis < _timeout);
return -1; // 시간 초과 시 -1 반환
}
- 데이터를 확인(peek)하지만 버퍼에서 제거하지 않음.
- _timeout 내에서 데이터가 도착하지 않으면 -1 반환.
3. find()
bool Stream::find(const char *target) {
return findUntil(target, strlen(target), NULL, 0);
}
- 입력 스트림에서 특정 문자열을 찾음.
- findUntil()을 호출하여 일치하는 문자열이 있는지 확인.
4. findUntil()
bool Stream::findUntil(const char *target, const char *terminator) {
return findUntil(target, strlen(target), terminator, strlen(terminator));
}
- 목표 문자열(target)을 찾되, 특정 종료 문자열(terminator)이 나오면 검색 중단
bool Stream::findUntil(const char *target, size_t targetLen, const char *terminator, size_t termLen) {
if (terminator == NULL) {
MultiTarget t[1] = {{target, targetLen, 0}};
return findMulti(t, 1) == 0;
} else {
MultiTarget t[2] = {{target, targetLen, 0}, {terminator, termLen, 0}};
return findMulti(t, 2) == 0;
}
}
- 목표 문자열이 존재하면 true 반환, 시간 초과되거나 종료 문자가 먼저 나오면 false 반환.
5. parseInt()
long Stream::parseInt(LookaheadMode lookahead, char ignore) {
bool isNegative = false;
long value = 0;
int c;
c = peekNextDigit(lookahead, false); // 다음 숫자 가져오기
if (c < 0) {
return 0; // timeout 시 0 반환
}
do {
if ((char)c == ignore); // 무시할 문자 건너뛰기
else if (c == '-') {
isNegative = true;
} else if (c >= '0' && c <= '9') { // 숫자인 경우
value = value * 10 + c - '0';
}
read(); // 문자 소비
c = timedPeek();
} while ((c >= '0' && c <= '9') || (char)c == ignore);
if (isNegative) {
value = -value;
}
return value;
}
- 입력된 문자열에서 정수를 파싱하여 반환.
- - 부호가 있으면 음수 처리.
- 숫자가 아닌 문자를 만나면 종료.
6. parseFloat()
float Stream::parseFloat(LookaheadMode lookahead, char ignore) {
bool isNegative = false;
bool isFraction = false;
double value = 0.0;
int c;
double fraction = 1.0;
c = peekNextDigit(lookahead, true); // 실수 지원
if (c < 0) {
return 0; // timeout 시 0 반환
}
do {
if ((char)c == ignore);
else if (c == '-') {
isNegative = true;
} else if (c == '.') {
isFraction = true;
} else if (c >= '0' && c <= '9') {
if (isFraction) {
fraction *= 0.1;
value = value + fraction * (c - '0');
} else {
value = value * 10 + c - '0';
}
}
read(); // 문자 소비
c = timedPeek();
} while ((c >= '0' && c <= '9') || (c == '.' && !isFraction) || (char)c == ignore);
if (isNegative) {
value = -value;
}
return value;
}
- 소수점(.)을 감지하여 실수를 반환.
- parseInt()와 유사하지만 소수점 뒤 숫자를 따로 계산.
7. readBytes()
size_t Stream::readBytes(char *buffer, size_t length) {
size_t count = 0;
while (count < length) {
int c = timedRead();
if (c < 0) {
break; // timeout 발생 시 종료
}
*buffer++ = (char)c;
count++;
}
return count; // 읽은 데이터 개수 반환
}
- 버퍼에 length만큼 데이터를 채움.
- 타임아웃 시 중단.
8. readBytesUntil()
size_t Stream::readBytesUntil(char terminator, char *buffer, size_t length) {
size_t index = 0;
while (index < length) {
int c = timedRead();
if (c < 0 || (char)c == terminator) {
break;
}
*buffer++ = (char)c;
index++;
}
return index; // 읽은 데이터 개수 반환
}
- 특정 문자(terminator)를 만나면 읽기를 중단.
9. readString() / readStringUntil()
String Stream::readString() {
String ret;
int c = timedRead();
while (c >= 0) {
ret += (char)c;
c = timedRead();
}
return ret;
}
- 입력된 데이터를 문자열로 읽음.
- readStringUntil(char terminator)는 특정 문자에서 중단됨.
Serial 클래스 주요 함수 요약표

함수명 | 용도 | 인수 |
begin(long baudRate) | 시리얼 통신 시작 | baudRate (전송 속도) |
begin(long baudRate, uint32_t config) | 통신 설정 포함 시작 | baudRate, config (데이터 비트, 패리티 등) |
begin(long baudRate, uint32_t config, int rxPin, int txPin) | 특정 핀을 RX/TX로 사용 | baudRate, config, rxPin, txPin |
end() | 시리얼 종료 | 없음 |
write(uint8_t byte) | 한 바이트 전송 | byte (전송할 데이터) |
write(const uint8_t *buffer, size_t size) | 바이트 배열 전송 | buffer (데이터 포인터), size (길이) |
print(any data) | 데이터 출력 | data (문자, 숫자, 문자열 등) |
println(any data) | 데이터 출력 + 줄바꿈 | data (문자, 숫자, 문자열 등) |
available() | 수신된 데이터 개수 확인 | 없음 |
read() | 한 바이트 읽기 | 없음 |
readBytes(char *buffer, size_t length) | 여러 바이트 읽기 | buffer (저장할 배열), length (읽을 개수) |
readString() | 문자열 읽기 | 없음 |
parseInt() | 정수 읽기 | 없음 |
parseFloat() | 실수 읽기 | 없음 |
peek() | 다음 바이트 미리보기 (제거하지 않음) | 없음 |
flush() | 송신 버퍼 비우기 | 없음 |
setRxBufferSize(size_t new_size) | RX 버퍼 크기 설정 (ESP32) | new_size (버퍼 크기) |
swap() | UART0 핀 변경 (ESP32) | 없음 |
Tip. Serial 수신버퍼 완전히 비우기
while (Serial.available()) Serial.read(); // 수신 버퍼를 완전히 비운다.
// 기존에 쌓여 있던 잘못된 데이터를 제거한 후, 새로운 데이터 수신을 시작할 수 있어 패킷이 깨지는 문제를 방지할 수 있다.
- 소프트웨어 시리얼을 사용할 때, 수신 버퍼에 예상치 못한 데이터가 쌓이는 현상이 발생할 수 있다.
- 이는 수신 핀의 플로팅(부동) 현상으로 인해 노이즈나 불필요한 신호가 입력되면서 발생하는 문제로 추정된다.
- 만약 수신 데이터가 깨지거나 예상과 다르게 밀려서 쌓이는 경우, 요청 패킷을 보내기 전에 Serial 수신 버퍼를 완전히 비워주는 방법으로 문제를 해결할 수 있다.
- 이러한 문제가 발생하면, 수신 버퍼에서 데이터를 읽어올 때 데이터가 밀려서 올바른 패킷을 해석하지 못하는 경우가 생길 수 있다. 특히, 소프트웨어 시리얼은 하드웨어 시리얼보다 안정성이 떨어지기 때문에 이러한 현상이 더 자주 발생할 수 있다.
'임베디드 소프트웨어 > 펌웨어 구현' 카테고리의 다른 글
통신 펌웨어 구현 가이드 (0) | 2025.03.15 |
---|---|
DWIN DGUS HMI 개발 라이브러리 (0) | 2025.03.02 |
FreeRTOS 사용하기 (2) | 2024.10.16 |
실시간 운영체제 (RTOS) (1) | 2024.09.04 |
[GUI & 터치스크린] LVGL 구현 관련 정리 (0) | 2024.07.31 |