Raspberry Pi 4와 Arduino Uno시리얼 통신 #2

11. C++ 시리얼 통신 예제

Ubuntu 22.04 Server에서 C++로 시리얼 통신을 하려면 Linux의 termios를 사용하면 됩니다.

C++ 파일을 만듭니다.

cd ~/serial_test
nano serial_cpp_test.cpp

아래 코드를 저장합니다.

/*
Raspberry Pi Ubuntu 22.04 C++ Serial Communication Test

동작:
1. Arduino Uno에서 오는 문자열을 읽어서 터미널에 출력
2. Arduino Uno로 문자열을 다시 전송
3. Arduino Uno는 수신 후 LED를 깜빡임

Compile:
g++ serial_cpp_test.cpp -o serial_cpp_test

Run:
./serial_cpp_test
*/

#include <iostream>
#include <string>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <chrono>
#include <thread>

const char* SERIAL_PORT = "/dev/ttyACM0";

int openSerialPort(const char* portName)
{
int serialFd = open(portName, O_RDWR | O_NOCTTY | O_SYNC);

if (serialFd < 0)
{
std::cerr << "Failed to open serial port: " << portName << std::endl;
return -1;
}

return serialFd;
}

bool configureSerialPort(int serialFd)
{
struct termios tty;

if (tcgetattr(serialFd, &tty) != 0)
{
std::cerr << "Failed to get serial port attributes." << std::endl;
return false;
}

cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);

tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;

tty.c_iflag &= ~IGNBRK;
tty.c_lflag = 0;
tty.c_oflag = 0;

tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 10;

tty.c_iflag &= ~(IXON | IXOFF | IXANY);

tty.c_cflag |= (CLOCAL | CREAD);

tty.c_cflag &= ~(PARENB | PARODD);
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CRTSCTS;

if (tcsetattr(serialFd, TCSANOW, &tty) != 0)
{
std::cerr << "Failed to set serial port attributes." << std::endl;
return false;
}

return true;
}

int main()
{
std::cout << "C++ Serial Test Start" << std::endl;
std::cout << "Port: " << SERIAL_PORT << std::endl;
std::cout << "Baud rate: 115200" << std::endl;

int serialFd = openSerialPort(SERIAL_PORT);

if (serialFd < 0)
{
return 1;
}

if (!configureSerialPort(serialFd))
{
close(serialFd);
return 1;
}

std::cout << "Serial port opened." << std::endl;

std::this_thread::sleep_for(std::chrono::seconds(2));

std::string lineBuffer;
int sendCount = 0;

while (true)
{
char readBuffer[256];
memset(readBuffer, 0, sizeof(readBuffer));

int numBytes = read(serialFd, readBuffer, sizeof(readBuffer) - 1);

if (numBytes > 0)
{
for (int i = 0; i < numBytes; i++)
{
char c = readBuffer[i];

if (c == '\n')
{
if (!lineBuffer.empty())
{
if (!lineBuffer.empty() && lineBuffer.back() == '\r')
{
lineBuffer.pop_back();
}

std::cout << "Received from Arduino: " << lineBuffer << std::endl;

sendCount++;

std::string message = "PING_FROM_CPP:" + std::to_string(sendCount) + "\n";

int writtenBytes = write(serialFd, message.c_str(), message.length());

if (writtenBytes > 0)
{
std::cout << "Sent to Arduino: " << message;
}

lineBuffer.clear();
}
}
else
{
lineBuffer += c;
}
}
}

std::this_thread::sleep_for(std::chrono::milliseconds(50));
}

close(serialFd);

return 0;
}

12. C++ 코드 설명

1) 헤더 파일 설명

#include <iostream>
#include <string>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <chrono>
#include <thread>

C++에서는 필요한 기능을 사용하기 위해 헤더 파일을 포함해야 합니다.

<iostream>
#include <iostream>

터미널 출력에 사용합니다.

예를 들어 다음 코드가 <iostream>을 사용합니다.

std::cout << "C++ Serial Test Start" << std::endl;
std::cerr << "Failed to open serial port" << std::endl;

std::cout은 일반 출력입니다.
std::cerr는 에러 출력입니다.

<string>
#include <string>

C++ 문자열인 std::string을 사용하기 위해 필요합니다.

이 코드에서는 다음 부분에서 사용합니다.

std::string lineBuffer;
std::string message = "PING_FROM_CPP:" + std::to_string(sendCount) + "\n";
<cstring>
#include <cstring>

C 스타일 문자열이나 메모리 관련 함수를 사용하기 위해 필요합니다.

이 코드에서는 memset()을 사용합니다.

memset(readBuffer, 0, sizeof(readBuffer));

readBuffer 배열을 0으로 초기화하는 역할입니다.

<fcntl.h>
#include <fcntl.h>

Linux에서 파일이나 장치를 열 때 사용하는 open() 함수와 관련 옵션을 사용하기 위해 필요합니다.

이 코드에서는 시리얼 포트를 열 때 사용합니다.

open(portName, O_RDWR | O_NOCTTY | O_SYNC);
<unistd.h>
#include <unistd.h>

Linux 시스템 함수인 read(), write(), close()를 사용하기 위해 필요합니다.

read(serialFd, readBuffer, sizeof(readBuffer) - 1);
write(serialFd, message.c_str(), message.length());
close(serialFd);

Linux에서는 시리얼 포트도 파일처럼 다루기 때문에 read(), write()를 사용합니다.

<termios.h>
#include <termios.h>

시리얼 포트 속도, 데이터 비트, 패리티, 스톱 비트 등을 설정하기 위해 필요합니다.

이 코드에서 가장 중요한 헤더 중 하나입니다.

struct termios tty;
cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);
tcsetattr(serialFd, TCSANOW, &tty);
<chrono><thread>
#include <chrono>
#include <thread>

시간 지연을 만들기 위해 사용합니다.

std::this_thread::sleep_for(std::chrono::seconds(2));
std::this_thread::sleep_for(std::chrono::milliseconds(50));

Python 코드의 time.sleep()과 비슷한 역할입니다.

2) 시리얼 포트 이름 설정

const char* SERIAL_PORT = "/dev/ttyACM1";

Arduino Uno가 Raspberry Pi에 USB로 연결되면 보통 /dev/ttyACM0로 잡힙니다.

그런데 OpenCR 보드가 연결되어 있어 /dev/ttyACM1로 잡힙니다.

Linux에서는 시리얼 장치를 파일처럼 다룹니다.

/dev/ttyACM1

이 파일을 열어서 Arduino와 통신하는 것입니다.

만약 Arduino가 /dev/ttyUSB0로 잡혔다면 다음처럼 바꿔야 합니다.

const char* SERIAL_PORT = "/dev/ttyUSB0";

포트 확인 명령은 다음과 같습니다.

ls /dev/ttyACM*

또는

ls /dev/ttyUSB*

연결 로그는 다음으로 확인할 수 있습니다.

dmesg | grep tty

3) openSerialPort() 함수 설명

int openSerialPort(const char* portName)
{
int serialFd = open(portName, O_RDWR | O_NOCTTY | O_SYNC);

if (serialFd < 0)
{
std::cerr << "Failed to open serial port: " << portName << std::endl;
return -1;
}

return serialFd;
}

이 함수는 시리얼 포트를 여는 역할을 합니다.

함수 입력값
const char* portName

열고 싶은 시리얼 포트 이름입니다.

현재 코드에서는 다음 값이 들어갑니다.

/dev/ttyACM1
open() 함수
int serialFd = open(portName, O_RDWR | O_NOCTTY | O_SYNC);

Linux에서 시리얼 포트는 파일처럼 열 수 있습니다.

open()이 성공하면 파일 디스크립터 번호를 반환합니다.

예를 들어 성공하면 다음과 같은 정수 값이 들어올 수 있습니다.

3
4
5

실패하면 -1을 반환합니다.

옵션 설명
O_RDWR

읽기와 쓰기 모두 가능하게 연다는 뜻입니다.

이 예제는 Arduino에서 데이터를 읽고, 다시 Arduino로 데이터를 보내야 하므로 읽기와 쓰기가 모두 필요합니다.

O_NOCTTY

이 시리얼 포트를 현재 프로그램의 제어 터미널로 사용하지 않겠다는 의미입니다.

시리얼 통신 프로그램에서는 보통 넣어주는 옵션입니다.

O_SYNC

쓰기 동작을 가능한 동기적으로 처리하라는 의미입니다.

간단히 말하면 데이터를 보낼 때 안정적으로 처리하도록 하는 옵션입니다.

실패 처리
if (serialFd < 0)
{
std::cerr << "Failed to open serial port: " << portName << std::endl;
return -1;
}

시리얼 포트를 열지 못하면 에러 메시지를 출력하고 -1을 반환합니다.

실패 원인은 보통 다음 중 하나입니다.

Arduino가 연결되어 있지 않음
포트 이름이 틀림
권한이 없음
Arduino IDE Serial Monitor가 이미 포트를 사용 중
Python 프로그램이 이미 포트를 사용 중
성공 처리
return serialFd;

성공하면 열린 시리얼 포트의 파일 디스크립터를 반환합니다.

이후 코드에서는 이 값을 이용해 read(), write(), close()를 수행합니다.

4) configureSerialPort() 함수 설명

bool configureSerialPort(int serialFd)

이 함수는 열린 시리얼 포트의 통신 조건을 설정합니다.

시리얼 통신에서는 양쪽 설정이 같아야 합니다.

이번 예제의 설정은 다음과 같습니다.

Baud rate: 115200
Data bits: 8
Parity: None
Stop bits: 1
Flow control: None

보통 줄여서 다음처럼 부릅니다.

115200 8N1

5) termios 구조체 생성

struct termios tty;

termios는 Linux에서 시리얼 포트 설정을 담는 구조체입니다.

이 구조체 안에는 다음과 같은 설정이 들어갑니다.

통신 속도
데이터 비트 수
패리티 사용 여부
스톱 비트 수
흐름 제어 여부
읽기 timeout 조건

6) 현재 시리얼 설정 읽기

if (tcgetattr(serialFd, &tty) != 0)
{
std::cerr << "Failed to get serial port attributes." << std::endl;
return false;
}

tcgetattr()는 현재 시리얼 포트 설정을 읽어오는 함수입니다.

읽어온 설정은 tty 구조체에 저장됩니다.

먼저 기존 설정을 읽은 뒤 필요한 부분만 바꾸는 방식입니다.

실패하면 false를 반환해서 설정 실패를 알립니다.

7) 통신 속도 설정

cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);

시리얼 통신 속도를 115200 bps로 설정합니다.

cfsetospeed(&tty, B115200);

송신 속도 설정입니다.

cfsetispeed(&tty, B115200);

수신 속도 설정입니다.

Arduino 코드에도 다음 설정이 있어야 합니다.

Serial.begin(115200);

C++ 쪽은 115200인데 Arduino 쪽이 9600이면 정상 통신이 되지 않습니다.

8) 데이터 비트 8비트 설정

tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;

이 코드는 데이터 비트를 8비트로 설정합니다.

시리얼 통신에서는 한 글자를 보낼 때 몇 비트 단위로 보낼지 정합니다.

일반적인 설정은 8비트입니다.

Data bits: 8

CSIZE는 데이터 비트 크기 설정 부분을 의미합니다.

tty.c_cflag & ~CSIZE

기존 데이터 비트 설정을 지웁니다.

| CS8

새로 8비트 설정을 적용합니다.

9) 입력 처리 관련 설정

tty.c_iflag &= ~IGNBRK;
tty.c_lflag = 0;
tty.c_oflag = 0;

이 부분은 시리얼 데이터를 최대한 원시 데이터에 가깝게 읽기 위한 설정입니다.

tty.c_iflag &= ~IGNBRK;
tty.c_iflag &= ~IGNBRK;

break 신호를 무시하지 않도록 설정합니다.

단순 Arduino 통신에서는 크게 신경 쓰지 않아도 되지만, 기본적인 시리얼 설정 과정에 포함됩니다.

tty.c_lflag = 0;
tty.c_lflag = 0;

로컬 모드 설정을 끕니다.

터미널 입력처럼 처리되는 기능을 끄는 것입니다.

일반 터미널에서는 엔터, 백스페이스, 에코 같은 기능이 있습니다.
하지만 시리얼 통신에서는 그런 터미널 기능이 오히려 방해될 수 있습니다.

그래서 raw 방식에 가깝게 읽도록 설정합니다.

tty.c_oflag = 0;
tty.c_oflag = 0;

출력 처리 기능을 끕니다.

예를 들어 줄바꿈 변환 같은 자동 처리를 하지 않도록 합니다.

10) 읽기 대기 조건 설정

tty.c_cc[VMIN] = 0;
tty.c_cc[VTIME] = 10;

이 부분은 read() 함수가 데이터를 기다리는 방식을 정합니다.

VMIN = 0
tty.c_cc[VMIN] = 0;

최소 몇 바이트가 들어와야 read()가 반환될지를 정합니다.

0이면 데이터가 없어도 timeout 조건에 따라 반환될 수 있습니다.

VTIME = 10
tty.c_cc[VTIME] = 10;

읽기 timeout 시간입니다.

termios에서 VTIME 단위는 0.1초입니다.

VTIME = 10 → 1.0초

즉, 현재 설정은 다음과 같습니다.

데이터가 있으면 읽는다.
데이터가 없으면 최대 1초 정도 기다린 뒤 반환한다.

이 설정 덕분에 read()가 영원히 멈춰 있지 않습니다.

11) 소프트웨어 흐름 제어 끄기

tty.c_iflag &= ~(IXON | IXOFF | IXANY);

이 코드는 XON/XOFF 소프트웨어 흐름 제어를 끕니다.

흐름 제어는 데이터 송수신을 잠시 멈추거나 다시 시작하게 하는 기능입니다.

Arduino Uno와 단순 USB 시리얼 통신을 할 때는 보통 사용하지 않습니다.

그래서 꺼둡니다.

Flow control: None

12) 수신 활성화 설정

tty.c_cflag |= (CLOCAL | CREAD);

이 부분은 시리얼 포트를 정상적으로 통신에 사용하기 위한 설정입니다.

CLOCAL
CLOCAL

모뎀 제어선 상태에 영향을 받지 않도록 합니다.

Arduino와 USB 시리얼 통신에서는 모뎀 제어 신호를 직접 다루지 않으므로 보통 켜둡니다.

CREAD
CREAD

수신 기능을 활성화합니다.

이 설정이 있어야 read()로 데이터를 받을 수 있습니다.

13) 패리티 끄기

tty.c_cflag &= ~(PARENB | PARODD);

패리티 검사를 사용하지 않도록 설정합니다.

이번 통신 설정은 다음과 같습니다.

Parity: None

패리티는 데이터 오류 검사용 비트입니다.
하지만 Arduino와 Raspberry Pi 사이의 기본 시리얼 통신에서는 보통 사용하지 않습니다.

14) 스톱 비트 1개 설정

tty.c_cflag &= ~CSTOPB;

스톱 비트를 1개로 설정합니다.

시리얼 통신에서 한 글자의 끝을 표시하는 비트가 스톱 비트입니다.

일반적인 설정은 1개입니다.

Stop bits: 1

CSTOPB가 설정되면 스톱 비트 2개를 의미합니다.
따라서 ~CSTOPB로 꺼주면 스톱 비트 1개가 됩니다.

15) 하드웨어 흐름 제어 끄기

tty.c_cflag &= ~CRTSCTS;

RTS/CTS 하드웨어 흐름 제어를 끕니다.

Arduino Uno와 USB 시리얼 통신에서는 RTS/CTS 흐름 제어를 사용하지 않습니다.

그래서 꺼두는 것이 맞습니다.

16) 설정 적용

if (tcsetattr(serialFd, TCSANOW, &tty) != 0)
{
std::cerr << "Failed to set serial port attributes." << std::endl;
return false;
}

tcsetattr()는 변경한 시리얼 설정을 실제 포트에 적용하는 함수입니다.

TCSANOW

는 설정을 즉시 적용하라는 의미입니다.

실패하면 에러 메시지를 출력하고 false를 반환합니다.

성공하면 아래 코드로 넘어갑니다.

return true;

17) main() 함수 시작

int main()
{

C++ 프로그램은 main() 함수에서 시작합니다.

이 프로그램의 전체 흐름은 다음과 같습니다.

1. 시작 메시지 출력
2. 시리얼 포트 열기
3. 시리얼 포트 설정
4. Arduino 리셋 대기
5. Arduino 데이터 읽기
6. 터미널에 출력
7. Arduino로 응답 전송
8. 반복

18) 시작 정보 출력

std::cout << "C++ Serial Test Start" << std::endl;
std::cout << "Port: " << SERIAL_PORT << std::endl;
std::cout << "Baud rate: 115200" << std::endl;

프로그램 시작 시 현재 설정 정보를 출력합니다.

실행하면 다음처럼 보입니다.

C++ Serial Test Start
Port: /dev/ttyACM0
Baud rate: 115200

이 출력은 강의에서 중요합니다.

수강생이 현재 어떤 포트를 사용하고 있는지, 통신 속도가 얼마인지 바로 확인할 수 있습니다.

19) 시리얼 포트 열기

int serialFd = openSerialPort(SERIAL_PORT);

앞에서 만든 openSerialPort() 함수를 호출합니다.

성공하면 serialFd에 파일 디스크립터가 저장됩니다.

실패하면 -1이 들어옵니다.

if (serialFd < 0)
{
return 1;
}

포트 열기에 실패하면 프로그램을 종료합니다.

return 1은 비정상 종료를 의미합니다.

20) 시리얼 포트 설정

if (!configureSerialPort(serialFd))
{
close(serialFd);
return 1;
}

시리얼 포트를 열었다고 바로 통신할 수 있는 것은 아닙니다.

통신 속도, 데이터 비트, 패리티, 스톱 비트 등을 설정해야 합니다.

configureSerialPort()가 실패하면 열린 포트를 닫고 프로그램을 종료합니다.

close(serialFd);

이 부분은 중요합니다.
열어둔 포트를 정리하지 않고 종료하는 것을 방지합니다.

21) 포트 열림 출력

std::cout << "Serial port opened." << std::endl;

이 메시지가 보이면 다음 두 가지가 성공한 것입니다.

1. /dev/ttyACM0 포트 열기 성공
2. 115200 8N1 시리얼 설정 성공

22) Arduino 준비 대기

std::this_thread::sleep_for(std::chrono::seconds(2));

Arduino Uno는 USB 시리얼 포트가 열릴 때 자동으로 리셋되는 경우가 많습니다.

즉, C++ 프로그램이 /dev/ttyACM0를 여는 순간 Arduino가 다시 시작될 수 있습니다.

Arduino가 다시 시작되면 setup() 함수가 실행됩니다.

이때 바로 통신을 시작하면 첫 메시지를 놓칠 수 있습니다.

그래서 2초 정도 기다립니다.

23) 수신 문자열 저장 버퍼

std::string lineBuffer;

Arduino에서 들어오는 데이터를 한 줄씩 저장하기 위한 문자열 버퍼입니다.

시리얼 데이터는 항상 한 줄 전체가 한 번에 들어온다고 보장할 수 없습니다.

예를 들어 Arduino가 다음을 보냈다고 해도,

ARDUINO_COUNT:1

C++의 read()는 내부적으로 이렇게 나눠 받을 수 있습니다.

ARDU
INO_COUNT
:1

그래서 받은 문자들을 lineBuffer에 계속 모읍니다.

줄바꿈 문자 \n을 만나면 한 줄이 끝났다고 판단하고 처리합니다.

24) 송신 카운트 변수

int sendCount = 0;

Raspberry Pi가 Arduino로 몇 번째 메시지를 보내는지 세기 위한 변수입니다.

Arduino에서 데이터를 한 줄 받을 때마다 1씩 증가합니다.

예를 들어 다음처럼 전송됩니다.

PING_FROM_CPP:1
PING_FROM_CPP:2
PING_FROM_CPP:3

25) 무한 반복 시작

while (true)
{

이 프로그램은 계속해서 Arduino 데이터를 읽어야 하므로 무한 반복 구조를 사용합니다.

사용자가 터미널에서 Ctrl + C를 누르기 전까지 계속 실행됩니다.

반복문 안에서는 다음 일을 합니다.

1. 읽기 버퍼 초기화
2. Arduino 데이터 읽기
3. 한 글자씩 검사
4. 줄바꿈을 만나면 한 줄 처리
5. Arduino로 응답 전송
6. 50ms 대기

26) 읽기 버퍼 생성 및 초기화

char readBuffer[256];
memset(readBuffer, 0, sizeof(readBuffer));

readBuffer는 시리얼 포트에서 읽은 데이터를 임시로 담는 배열입니다.

크기는 256바이트입니다.

char readBuffer[256];

memset()은 배열 내용을 0으로 초기화합니다.

memset(readBuffer, 0, sizeof(readBuffer));

초기화하지 않아도 동작할 수는 있지만, 강의용 코드에서는 버퍼를 깨끗하게 만들고 사용하는 편이 이해하기 좋습니다.

27) 시리얼 데이터 읽기

int numBytes = read(serialFd, readBuffer, sizeof(readBuffer) - 1);

이 줄이 Arduino에서 데이터를 읽는 핵심입니다.

read()

는 Linux 시스템 함수입니다.

여기서는 /dev/ttyACM0에서 데이터를 읽습니다.

인자 설명
serialFd

열려 있는 시리얼 포트의 파일 디스크립터입니다.

readBuffer

읽은 데이터를 저장할 배열입니다.

sizeof(readBuffer) - 1

최대 읽을 바이트 수입니다.

readBuffer 크기는 256이지만 255바이트까지만 읽습니다.

마지막 1바이트를 남겨두는 이유는 문자열 끝에 \0이 들어갈 공간을 여유로 두는 습관 때문입니다.

이 코드에서는 한 글자씩 처리하므로 큰 문제는 없지만 안전한 방식입니다.

반환값
int numBytes

read()가 실제로 읽은 바이트 수입니다.

예를 들어 18바이트를 읽었다면 다음과 같습니다.

numBytes = 18

읽은 데이터가 없으면 0이거나 음수 값이 될 수 있습니다.

28) 읽은 데이터가 있을 때만 처리

if (numBytes > 0)
{

실제로 읽은 데이터가 있을 때만 처리합니다.

데이터가 없으면 아무것도 하지 않고 아래쪽의 50ms 대기로 넘어갑니다.

29) 읽은 데이터를 한 글자씩 확인

for (int i = 0; i < numBytes; i++)
{
char c = readBuffer[i];

read()는 여러 문자를 한 번에 읽을 수 있습니다.

예를 들어 readBuffer 안에 다음 데이터가 들어올 수 있습니다.

ARDUINO_COUNT:1\n

이 코드는 이 데이터를 한 글자씩 꺼내서 검사합니다.

A
R
D
U
I
N
O
_
C
O
U
N
T
:
1
\n

한 글자씩 검사하는 이유는 \n을 기준으로 한 줄을 나누기 위해서입니다.

30) 줄바꿈 문자 확인

if (c == '\n')

Arduino 코드에서는 Serial.println()을 사용합니다.

Serial.println("ARDUINO_COUNT:1");

println()은 문자열 끝에 줄바꿈 문자를 붙입니다.

ARDUINO_COUNT:1\n

C++ 코드는 이 \n을 만나면 한 줄이 끝났다고 판단합니다.

즉, \n은 데이터의 끝을 알려주는 구분자입니다.

31) 비어 있지 않은 줄만 처리

if (!lineBuffer.empty())
{

lineBuffer에 실제 문자열이 들어 있을 때만 처리합니다.

빈 줄이 들어온 경우에는 무시합니다.

예를 들어 다음처럼 의미 없는 줄바꿈만 들어오면 처리하지 않습니다.

\n

32) \r 문자 제거

if (!lineBuffer.empty() && lineBuffer.back() == '\r')
{
lineBuffer.pop_back();
}

시리얼 통신에서는 줄 끝이 \n만 오는 경우도 있고, \r\n으로 오는 경우도 있습니다.

Linux 스타일: \n
Windows 스타일: \r\n

Arduino의 Serial.println()은 환경에 따라 \r\n처럼 보일 수 있습니다.

C++ 코드는 \n을 기준으로 줄 끝을 판단합니다.
그런데 그 앞에 \r이 남을 수 있습니다.

예를 들어 받은 데이터가 다음과 같다면,

ARDUINO_COUNT:1\r\n

\n을 만나기 전까지 lineBuffer에는 다음이 들어 있습니다.

ARDUINO_COUNT:1\r

그래서 마지막 문자가 \r이면 제거합니다.

lineBuffer.pop_back();

그 결과 깔끔하게 다음 문자열만 남습니다.

ARDUINO_COUNT:1

33) 받은 데이터 출력

std::cout << "Received from Arduino: " << lineBuffer << std::endl;

Arduino에서 받은 한 줄의 데이터를 터미널에 출력합니다.

예를 들어 Arduino가 다음을 보냈다면,

ARDUINO_COUNT:1

Raspberry Pi 터미널에는 다음처럼 출력됩니다.

Received from Arduino: ARDUINO_COUNT:1

이 출력이 보이면 다음 사실을 확인할 수 있습니다.

Arduino가 데이터를 보냈다.
USB 시리얼 통신이 연결되어 있다.
Raspberry Pi C++ 코드가 데이터를 받았다.

34) 송신 카운트 증가

sendCount++;

Arduino에서 한 줄을 받았으므로 Arduino로 보낼 응답 번호를 1 증가시킵니다.

처음에는 sendCount = 0입니다.

첫 번째 수신 후에는 다음이 됩니다.

sendCount = 1

두 번째 수신 후에는 다음이 됩니다.

sendCount = 2

35) Arduino로 보낼 메시지 생성

std::string message = "PING_FROM_CPP:" + std::to_string(sendCount) + "\n";

Arduino로 보낼 문자열을 만듭니다.

예를 들어 sendCount가 1이면 다음 문자열이 만들어집니다.

PING_FROM_CPP:1\n

여기서 마지막 \n이 중요합니다.

Arduino 코드는 \n을 만나야 한 줄의 명령이 끝났다고 판단합니다.

Arduino 쪽 코드에는 다음 조건이 있습니다.

if (receivedChar == '\n')

따라서 C++에서 \n 없이 보내면 Arduino는 문자열이 끝나지 않았다고 보고 계속 기다릴 수 있습니다.

36) 숫자를 문자열로 변환

std::to_string(sendCount)

sendCount는 정수입니다.

int sendCount = 1;

문자열 "PING_FROM_CPP:"와 연결하려면 숫자를 문자열로 바꿔야 합니다.

그래서 std::to_string()을 사용합니다.

std::to_string(1)

결과는 다음 문자열입니다.

"1"

37) Arduino로 데이터 전송

int writtenBytes = write(serialFd, message.c_str(), message.length());

이 줄은 Raspberry Pi에서 Arduino로 데이터를 보내는 핵심입니다.

write()

Linux 시스템 함수입니다.

열려 있는 파일 또는 장치에 데이터를 씁니다.

여기서는 /dev/ttyACM1에 데이터를 쓰므로 Arduino로 데이터가 전송됩니다.

message.c_str()

message는 C++ 문자열인 std::string입니다.

write() 함수는 C 스타일 문자열 포인터를 필요로 합니다.

그래서 다음처럼 변환합니다.

message.c_str()
message.length()

보낼 데이터의 길이입니다.

예를 들어 메시지가 다음과 같다면,

PING_FROM_CPP:1\n

그 길이만큼 전송합니다.

반환값
int writtenBytes

실제로 전송된 바이트 수입니다.

0보다 크면 어느 정도 데이터가 전송된 것입니다.

38) 송신 메시지 출력

if (writtenBytes > 0)
{
std::cout << "Sent to Arduino: " << message;
}

Arduino로 데이터를 보낸 뒤 터미널에 출력합니다.

예시:

Sent to Arduino: PING_FROM_CPP:1

여기서 message 안에는 이미 \n이 들어 있습니다.

그래서 std::endl을 따로 붙이지 않아도 줄바꿈이 됩니다.

39) 줄 버퍼 초기화

lineBuffer.clear();

한 줄 처리가 끝났으므로 lineBuffer를 비웁니다.

다음 Arduino 메시지를 새로 받기 위해서입니다.

만약 이걸 하지 않으면 다음 메시지가 이전 메시지 뒤에 붙을 수 있습니다.

예를 들어 버퍼를 비우지 않으면 다음처럼 될 수 있습니다.

ARDUINO_COUNT:1ARDUINO_COUNT:2

40) 줄바꿈이 아닌 문자는 버퍼에 추가

else
{
lineBuffer += c;
}

현재 읽은 문자가 \n이 아니라면 아직 한 줄이 끝나지 않은 것입니다.

그래서 lineBuffer에 계속 추가합니다.

예를 들어 Arduino가 다음을 보낸다면,

ARDUINO_COUNT:1\n

버퍼에는 다음처럼 쌓입니다.

A
AR
ARD
ARDU
ARDUI
ARDUINO
ARDUINO_
ARDUINO_COUNT
ARDUINO_COUNT:1

마지막에 \n을 만나면 그때 한 줄로 처리합니다.

41) 반복 속도 조절

std::this_thread::sleep_for(std::chrono::milliseconds(50));

반복문이 너무 빠르게 돌지 않도록 50ms 기다립니다.

이 코드가 없으면 while (true)가 매우 빠르게 반복되면서 CPU 사용률이 불필요하게 올라갈 수 있습니다.

50ms는 시리얼 통신 테스트에는 충분히 짧은 시간입니다.

50ms = 0.05초

42) 포트 닫기

close(serialFd);

시리얼 포트를 닫는 코드입니다.

현재 코드는 while (true) 때문에 일반적으로 이 줄까지 도달하지 않습니다.

사용자가 Ctrl + C로 종료하면 운영체제가 자원을 정리합니다.

다만 코드 구조상으로는 포트를 열었으면 닫는 것이 맞기 때문에 포함되어 있습니다.

나중에 종료 조건을 추가하면 이 줄이 실제로 실행될 수 있습니다.

43) 프로그램 종료

return 0;

return 0은 프로그램이 정상 종료되었다는 의미입니다.

현재 예제는 무한 반복 구조라 보통 Ctrl + C로 종료합니다.

13. 실행

1) 컴파일러 설치 확인

g++ --version

만약 g++가 없다면 설치합니다.

sudo apt update
sudo apt install build-essential -y

2) 컴파일

g++ serial_cpp_test.cpp -o serial_cpp_test

컴파일 후 파일을 확인합니다.

ls

출력 예시:

serial_cpp_test.cpp  serial_cpp_test

3) 실행

./serial_cpp_test

4) 정상 출력 예시

C++ Serial Test Start
Port: /dev/ttyACM0
Baud rate: 115200
Serial port opened.
Received from Arduino: ARDUINO_READY
Sent to Arduino: PING_FROM_CPP:1
Received from Arduino: ARDUINO_COUNT:1
Sent to Arduino: PING_FROM_CPP:2
Received from Arduino: ARDUINO_RECEIVED:PING_FROM_CPP:1
Sent to Arduino: PING_FROM_CPP:3
Received from Arduino: ARDUINO_COUNT:2
Sent to Arduino: PING_FROM_CPP:4

이때 Arduino Uno의 내장 LED가 깜빡이면 정상입니다.

14. Python 방식과 C++ 방식 비교

항목PythonC++
난이도낮음중간
코드 길이짧음
라이브러리pyserialtermios
강의 초반 적합성매우 좋음시스템 이해용으로 좋음
ROS 2 노드 확장가능매우 적합
실시간 제어보통유리
디버깅 편의성좋음보통

Leave a Comment