Raspberry Pi 4와 Arduino Uno시리얼 통신

1. 전체 구조

USB 케이블을 사용하면 다음 두 가지가 동시에 처리됩니다.

1. Arduino 전원 공급
2. Raspberry Pi ↔ Arduino 시리얼 통신

별도의 센서, 브레드보드, 점퍼선은 필요 없습니다.

2. 통신 흐름

Arduino Uno → Raspberry Pi

Arduino sends:
ARDUINO_COUNT:1
ARDUINO_COUNT:2
ARDUINO_COUNT:3

Raspberry Pi terminal output:
Received from Arduino: ARDUINO_COUNT:1
Received from Arduino: ARDUINO_COUNT:2
Received from Arduino: ARDUINO_COUNT:3

즉, 핵심은 다음과 같습니다.

Arduino가 데이터를 보낸다.
Raspberry Pi가 받는다.
Raspberry Pi가 다시 데이터를 보낸다.
Arduino가 받으면 LED를 켠다.

3. 준비물

TurtleBot3 Burger
Raspberry Pi 4
Ubuntu 22.04 Server
Arduino Uno
USB A-B 케이블

Arduino Uno는 일반적으로 USB 연결 시 다음 장치명으로 잡힙니다.

/dev/ttyACM0

일부 보드는 다음처럼 잡힐 수도 있습니다.

/dev/ttyUSB0

4. Arduino Uno 연결 확인

Raspberry Pi에 Arduino Uno를 USB로 연결한 뒤 다음 명령을 실행합니다.

ls /dev/ttyACM*

출력 예시:

/dev/ttyACM1

또는 다음 명령으로 USB 장치를 확인할 수 있습니다.

sudo dmesg | grep tty

출력 예시:

[ 1234.567890] cdc_acm 1-1.2:1.0: ttyACM0: USB ACM device

5. 시리얼 포트 권한 설정

Ubuntu에서 일반 사용자가 /dev/ttyACM1에 접근하려면 dialout 그룹 권한이 필요합니다.

sudo usermod -a -G dialout $USER

적용하려면 로그아웃 후 다시 로그인하거나 재부팅합니다.

sudo reboot

재부팅 후 그룹 적용 여부를 확인합니다.

groups

출력에 dialout이 포함되어 있으면 정상입니다.

ubuntu adm dialout sudo ...

6. Arduino IDE 설치

Arduino 홈페이지의 다운로드 페이제로 접속합니다.

https://www.arduino.cc/en/software/https://www.arduino.cc/en/software/
www.arduino.cc

다운로드 완료 후 프로그램 실행 계속해서 기본 설치를 진행합니다.

바탕 화면에서 설치된 Arduino 프로그램 아이콘을 더블 클릭하여 실행합니다.

[파일] -> [환경설정] -> [줄 번호 표시] 등 필요한 옵션의 설정 변경을 변경합니다.

7. Arduino Uno 샘플 코드 테스트

Arduino Uno 보드의 USB 케이블을 PC에 연결합니다.

장치관리자를 열고 COM port의 정상적으로 장치 등록 여부를 확인합니다.

파일 -> 예제 -> 01.Basics -> Blink를 선택합니다.

새로운 창에 blink 예제 파일이 열립니다.

툴 -> 보드 -> Arduino UNO를 선택합니다.

포트는 장치관리자에서 확인한 COM port를 선택합니다.

왼쪽 컴파일 버튼과 업로드 버튼을 번갈아 클릭하면 보드에 실행파일이 업로드 되고 LED가 1초에 한번씩 깜박 거립니다.

delay(1000) -> delay(200)으로 수정하고 컴파일해서 업로드하면 깜박이는 속도가 빨라집니다.

7. Arduino Uno 코드

Arduino Uno에는 아래 코드를 업로드합니다.

이 코드는 다음 동작을 합니다.

1. 1초마다 카운트 데이터를 Raspberry Pi로 전송
2. Raspberry Pi에서 문자열을 받으면 터미널로 받은 내용 출력
3. 데이터를 받으면 내장 LED를 잠깐 켰다가 끔

Arduino Uno의 내장 LED는 보통 13번 핀에 연결되어 있습니다. Arduino에서는 LED_BUILTIN이라는 이름으로 사용할 수 있습니다.

/*
  Arduino Uno Serial Communication Test

  동작:
  1. Arduino가 1초마다 Raspberry Pi로 문자열 데이터 전송
  2. Raspberry Pi에서 문자열을 받으면 LED_BUILTIN을 잠깐 켬
  3. 받은 문자열을 다시 Serial Monitor 또는 Raspberry Pi 쪽으로 출력

  보드:
  Arduino Uno

  통신 속도:
  115200 bps
*/

const int ledPin = LED_BUILTIN;

unsigned long previousMillis = 0;
const unsigned long sendInterval = 1000;

int countValue = 0;

String rxBuffer = "";

void setup()
{
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  Serial.begin(115200);

  delay(1000);

  Serial.println("ARDUINO_READY");
}

void loop()
{
  sendPeriodicData();
  receiveSerialData();
}

void sendPeriodicData()
{
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= sendInterval)
  {
    previousMillis = currentMillis;

    countValue++;

    Serial.print("ARDUINO_COUNT:");
    Serial.println(countValue);
  }
}

void receiveSerialData()
{
  while (Serial.available() > 0)
  {
    char receivedChar = Serial.read();

    if (receivedChar == '\n')
    {
      rxBuffer.trim();

      if (rxBuffer.length() > 0)
      {
        Serial.print("ARDUINO_RECEIVED:");
        Serial.println(rxBuffer);

        blinkLed();
      }

      rxBuffer = "";
    }
    else
    {
      rxBuffer += receivedChar;
    }
  }
}

void blinkLed()
{
  digitalWrite(ledPin, HIGH);
  delay(200);
  digitalWrite(ledPin, LOW);
  delay(200);
}

8. Arduino 코드 설명

이 코드는 Arduino Uno와 Raspberry Pi가 USB 시리얼 통신으로 서로 데이터를 주고받는지 확인하는 테스트 코드입니다.

센서는 아직 연결하지 않습니다. 대신 Arduino가 가상의 센서 데이터처럼 ARDUINO_COUNT 값을 1초마다 보냅니다.

Raspberry Pi는 이 데이터를 받고, 다시 Arduino로 문자열을 보냅니다.
Arduino는 Raspberry Pi에서 문자열을 받으면 내장 LED를 잠깐 켜서 “데이터를 받았다”는 것을 눈으로 보여줍니다.

즉, 전체 동작은 다음과 같습니다.

Arduino Uno → Raspberry Pi
1초마다 ARDUINO_COUNT:1, ARDUINO_COUNT:2 ... 전송

Raspberry Pi → Arduino Uno
PING_FROM_PYTHON 또는 PING_FROM_CPP 같은 문자열 전송

Arduino Uno
문자열을 받으면 내장 LED 깜빡임

1) LED 핀 설정

const int ledPin = LED_BUILTIN;

Arduino Uno에는 보드 위에 기본 LED가 하나 있습니다.
이 LED는 보통 디지털 13번 핀에 연결되어 있습니다.

Arduino에서는 이 내장 LED를 LED_BUILTIN이라는 이름으로 사용할 수 있습니다.

즉, 위 코드는 다음 의미입니다.

ledPin이라는 이름으로 Arduino 내장 LED를 사용하겠다.

굳이 외부 LED를 연결하지 않아도 됩니다.
USB 케이블과 Arduino Uno만 있어도 LED 동작을 확인할 수 있습니다.

2) 시간 측정용 변수

unsigned long previousMillis = 0;
const unsigned long sendInterval = 1000;

이 두 변수는 1초마다 데이터를 보내기 위해 사용하는 변수입니다.

previousMillis
unsigned long previousMillis = 0;

이 변수는 마지막으로 데이터를 보낸 시간을 저장합니다.

예를 들어 Arduino가 3초 시점에 데이터를 보냈다면, previousMillis에는 대략 3000이라는 값이 저장됩니다.

sendInterval
const unsigned long sendInterval = 1000;

이 값은 데이터를 보내는 간격입니다.

Arduino에서 시간 단위는 보통 밀리초입니다.

1000 ms = 1초

따라서 이 코드는 다음 의미입니다.

1초마다 데이터를 보내겠다.

만약 0.5초마다 보내고 싶다면 이렇게 바꿀 수 있습니다.

const unsigned long sendInterval = 500;

2초마다 보내고 싶다면 이렇게 바꿉니다.

const unsigned long sendInterval = 2000;

3) 카운트 변수

int countValue = 0;

이 변수는 Arduino가 Raspberry Pi로 보내는 숫자 값을 저장합니다.

처음에는 0입니다.

그 후 1초마다 1씩 증가합니다.

1
2
3
4
5

그래서 실제 전송 데이터는 다음처럼 됩니다.

ARDUINO_COUNT:1
ARDUINO_COUNT:2
ARDUINO_COUNT:3

센서가 없는 상태에서 통신 테스트를 하기 위해 사용하는 가짜 데이터라고 보면 됩니다.

나중에 센서를 연결하면 countValue 대신 센서 값을 보내면 됩니다.

예를 들어 거리 센서라면 다음처럼 바꿀 수 있습니다.

int distance = analogRead(A0);

Serial.print("DISTANCE:");
Serial.println(distance);

4) 수신 문자열 저장 변수

String rxBuffer = "";

rxBuffer는 Raspberry Pi에서 Arduino로 들어오는 문자열을 임시로 저장하는 변수입니다.

시리얼 통신 데이터는 한 번에 완성된 문장으로 들어온다고 보장할 수 없습니다.

예를 들어 Raspberry Pi가 다음 문자열을 보냈다고 가정합니다.

PING_FROM_PYTHON:1

Arduino 입장에서는 이 데이터가 내부적으로 다음처럼 한 글자씩 들어옵니다.

P
I
N
G
_
F
R
O
M
_
P
Y
T
H
O
N
:
1
\n

그래서 Arduino는 한 글자씩 읽으면서 rxBuffer에 모읍니다.

최종적으로 \n을 만나면,

아, 한 줄이 끝났구나.

라고 판단하고 rxBuffer 안에 모인 문자열을 처리합니다.

5) setup() 함수 설명

void setup()
{
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);

Serial.begin(115200);

delay(1000);

Serial.println("ARDUINO_READY");
}

setup() 함수는 Arduino가 처음 켜질 때 한 번만 실행됩니다.

전원 연결, 리셋 버튼, USB 연결 후 부팅될 때 실행됩니다.

LED 출력 모드 설정
pinMode(ledPin, OUTPUT);

LED 핀을 출력 모드로 설정합니다.

Arduino 핀은 입력으로도 쓸 수 있고 출력으로도 쓸 수 있습니다.

LED를 켜려면 Arduino가 핀에 전압을 내보내야 하므로 OUTPUT으로 설정해야 합니다.

LED 끄기
digitalWrite(ledPin, LOW);

처음 시작할 때 LED를 꺼둡니다.

LOW는 0V에 가까운 상태입니다.
대부분의 Arduino Uno 내장 LED는 LOW일 때 꺼집니다.

시리얼 통신 시작
Serial.begin(115200);

Arduino의 시리얼 통신을 시작합니다.

여기서 115200은 통신 속도입니다.

Raspberry Pi 쪽 코드도 반드시 같은 속도여야 합니다.

Arduino:      115200
Raspberry Pi: 115200

둘 중 하나라도 다르면 글자가 깨지거나 통신이 정상적으로 되지 않습니다.

1초 대기
delay(1000);

Arduino가 부팅된 직후 바로 데이터를 보내면 Raspberry Pi 쪽 프로그램이 아직 준비되지 않았을 수 있습니다.

그래서 1초 정도 기다린 뒤 준비 메시지를 보냅니다.

준비 완료 메시지 전송
Serial.println("ARDUINO_READY");

Arduino가 준비되었다는 메시지를 Raspberry Pi로 보냅니다.

Raspberry Pi 터미널에서는 다음처럼 보일 수 있습니다.

Received from Arduino: ARDUINO_READY

이 메시지가 보이면 Arduino 코드가 정상적으로 실행 중이라는 뜻입니다.

6) loop() 함수 설명

void loop()
{
sendPeriodicData();
receiveSerialData();
}

loop() 함수는 Arduino가 켜져 있는 동안 계속 반복 실행됩니다.

이 코드에서는 loop() 안에서 두 가지 함수를 계속 호출합니다.

sendPeriodicData();

Arduino가 Raspberry Pi로 데이터를 보내는 함수입니다.

receiveSerialData();

Raspberry Pi에서 Arduino로 들어오는 데이터를 확인하는 함수입니다.

즉, Arduino는 계속 반복하면서 다음 두 가지 일을 합니다.

보낼 시간이 되었는지 확인한다.
받은 데이터가 있는지 확인한다.

7) sendPeriodicData() 함수 설명

void sendPeriodicData()
{
unsigned long currentMillis = millis();

if (currentMillis - previousMillis >= sendInterval)
{
previousMillis = currentMillis;

countValue++;

Serial.print("ARDUINO_COUNT:");
Serial.println(countValue);
}
}

이 함수는 Arduino가 1초마다 Raspberry Pi로 데이터를 보내는 역할을 합니다.

현재 시간 읽기
unsigned long currentMillis = millis();

millis()는 Arduino가 켜진 뒤 지금까지 지난 시간을 밀리초 단위로 알려줍니다.

예를 들어 Arduino가 켜진 지 5초가 지났다면 대략 다음 값을 반환합니다.

5000
1초가 지났는지 확인
if (currentMillis - previousMillis >= sendInterval)

이 조건은 다음 뜻입니다.

현재 시간 - 마지막으로 보낸 시간 >= 1000ms

즉, 마지막으로 데이터를 보낸 뒤 1초 이상 지났는지 확인합니다.

예를 들어 다음 상황을 보겠습니다.

previousMillis = 3000
currentMillis = 4000
sendInterval = 1000

계산하면 다음과 같습니다.

4000 - 3000 = 1000

1초가 지났으므로 데이터를 보냅니다.

마지막 전송 시간 갱신
previousMillis = currentMillis;

데이터를 보냈으므로 마지막 전송 시간을 현재 시간으로 바꿉니다.

그래야 다음 1초를 다시 정확히 계산할 수 있습니다.

카운트 증가
countValue++;

countValue를 1 증가시킵니다.

처음에는 0이었으므로 첫 전송 때는 1이 됩니다.

0 → 1 → 2 → 3 → 4
문자열 전송
Serial.print("ARDUINO_COUNT:");
Serial.println(countValue);

이 두 줄은 합쳐서 한 줄의 문자열을 보냅니다.

예를 들어 countValue가 5라면 다음 데이터가 전송됩니다.

ARDUINO_COUNT:5

Serial.print()는 줄바꿈 없이 출력합니다.

Serial.print("ARDUINO_COUNT:");

Serial.println()은 출력 후 줄바꿈을 붙입니다.

Serial.println(countValue);

그래서 최종적으로는 다음처럼 전송됩니다.

ARDUINO_COUNT:5\n

여기서 \n은 줄바꿈 문자입니다.

Raspberry Pi의 Python이나 C++ 코드에서는 이 줄바꿈 문자를 기준으로 한 줄씩 데이터를 읽을 수 있습니다.

8) receiveSerialData() 함수 설명

void receiveSerialData()
{
while (Serial.available() > 0)
{
char receivedChar = Serial.read();

if (receivedChar == '\n')
{
rxBuffer.trim();

if (rxBuffer.length() > 0)
{
Serial.print("ARDUINO_RECEIVED:");
Serial.println(rxBuffer);

blinkLed();
}

rxBuffer = "";
}
else
{
rxBuffer += receivedChar;
}
}
}

이 함수는 Raspberry Pi에서 Arduino로 들어오는 데이터를 읽는 역할을 합니다.

받은 데이터가 있는지 확인
while (Serial.available() > 0)

Serial.available()은 현재 Arduino가 받을 수 있는 시리얼 데이터가 몇 바이트 있는지 알려줍니다.

예를 들어 받을 데이터가 없으면 다음과 같습니다.

Serial.available() = 0

받은 문자가 3개 쌓여 있으면 다음과 같습니다.

Serial.available() = 3

while문을 사용했기 때문에 쌓여 있는 데이터를 모두 읽을 때까지 반복합니다.

한 글자 읽기
char receivedChar = Serial.read();

시리얼 버퍼에서 문자 하나를 읽습니다.

예를 들어 Raspberry Pi가 다음 문자열을 보냈다고 가정합니다.

PING_FROM_PYTHON:1\n

Arduino는 한 번에 전체 문자열을 읽는 것이 아니라 보통 문자 단위로 처리합니다.

P
I
N
G
_
F
R
O
M
_
P
Y
T
H
O
N
:
1
\n
줄바꿈 문자인지 확인
if (receivedChar == '\n')

'\n'은 줄바꿈 문자입니다.

이 문자가 들어왔다는 것은 Raspberry Pi가 보낸 한 줄의 데이터가 끝났다는 뜻입니다.

예를 들어 Raspberry Pi에서 다음처럼 보냈다면,

PING_FROM_PYTHON:1\n

Arduino는 \n을 만났을 때 다음 문자열이 완성되었다고 판단합니다.

PING_FROM_PYTHON:1
앞뒤 공백 제거
rxBuffer.trim();

trim()은 문자열 앞뒤의 공백, 줄바꿈, 캐리지 리턴 등을 제거합니다.

예를 들어 문자열이 다음과 같다면,

" PING_FROM_PYTHON:1\r"

trim() 후에는 다음처럼 정리됩니다.

"PING_FROM_PYTHON:1"

Windows나 일부 시리얼 프로그램은 줄 끝에 \r\n을 붙일 수 있습니다.
그래서 trim()을 해두면 불필요한 문자를 줄일 수 있습니다.

실제 데이터가 있는지 확인
if (rxBuffer.length() > 0)

문자열 길이가 0보다 크면 실제로 받은 데이터가 있다는 뜻입니다.

빈 줄만 들어온 경우에는 처리하지 않습니다.

받은 데이터 다시 출력
Serial.print("ARDUINO_RECEIVED:");
Serial.println(rxBuffer);

Arduino가 받은 데이터를 다시 Raspberry Pi로 출력합니다.

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

PING_FROM_PYTHON:1

Arduino는 다음을 다시 보냅니다.

ARDUINO_RECEIVED:PING_FROM_PYTHON:1

이렇게 하는 이유는 Raspberry Pi 터미널에서 다음 사실을 확인하기 위해서입니다.

Raspberry Pi가 보낸 데이터가 Arduino에 도착했다.
Arduino가 그 데이터를 읽었다.
Arduino가 다시 응답했다.
LED 깜빡이기
blinkLed();

데이터를 정상적으로 받았으면 LED를 깜빡입니다.

이 LED는 통신 성공 표시등 역할을 합니다.

LED가 깜빡인다 = Arduino가 Raspberry Pi의 데이터를 받았다
수신 버퍼 비우기
rxBuffer = "";

한 줄의 명령 처리가 끝났으므로 버퍼를 비웁니다.

그래야 다음 문자열을 새로 받을 수 있습니다.

예를 들어 첫 번째 데이터 처리 후 버퍼를 비우지 않으면,

PING_FROM_PYTHON:1PING_FROM_PYTHON:2

처럼 데이터가 이어 붙을 수 있습니다.

9) else 부분 설명

else
{
rxBuffer += receivedChar;
}

읽은 문자가 줄바꿈 문자가 아니라면 아직 한 줄이 끝난 것이 아닙니다.

그래서 rxBuffer에 문자를 계속 추가합니다.

예를 들어 다음 문자열을 받는 과정은 이렇습니다.

수신 문자: P
rxBuffer: "P"

수신 문자: I
rxBuffer: "PI"

수신 문자: N
rxBuffer: "PIN"

수신 문자: G
rxBuffer: "PING"

계속 모으다가 \n을 만나면 그때 전체 문자열을 처리합니다.

10) blinkLed() 함수 설명

void blinkLed()
{
digitalWrite(ledPin, HIGH);
delay(200);
digitalWrite(ledPin, LOW);
}

이 함수는 LED를 잠깐 켰다가 끕니다.

LED 켜기
digitalWrite(ledPin, HIGH);

ledPin에 HIGH 신호를 줍니다.

Arduino Uno 내장 LED는 보통 이때 켜집니다.

0.2초 기다리기
delay(200);

200ms 동안 기다립니다.

200ms = 0.2초
LED 끄기
digitalWrite(ledPin, LOW);

LED를 끕니다.

이 함수는 Raspberry Pi에서 데이터가 들어왔을 때만 실행되므로, LED가 깜빡이면 수신이 된 것입니다.

11) 실제 실행 시 보이는 데이터 흐름

Arduino가 먼저 준비 메시지를 보냅니다.

ARDUINO_READY

그 다음 1초마다 카운트 값을 보냅니다.

ARDUINO_COUNT:1
ARDUINO_COUNT:2
ARDUINO_COUNT:3

Raspberry Pi의 Python 코드가 데이터를 받을 때마다 다음을 보낸다고 가정합니다.

PING_FROM_PYTHON:1
PING_FROM_PYTHON:2
PING_FROM_PYTHON:3

Arduino는 이것을 받고 다시 응답합니다.

ARDUINO_RECEIVED:PING_FROM_PYTHON:1
ARDUINO_RECEIVED:PING_FROM_PYTHON:2
ARDUINO_RECEIVED:PING_FROM_PYTHON:3

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

12) 중요한 개념 정리

Serial.print()Serial.println() 차이
Serial.print("ABC");
Serial.print("DEF");

출력:

ABCDEF
Serial.println("ABC");
Serial.println("DEF");

출력:

ABC
DEF

println()은 마지막에 줄바꿈을 추가합니다.

이번 예제에서는 줄 단위 통신을 하기 때문에 println()이 중요합니다.

\n의 역할

\n은 한 줄의 끝을 나타냅니다.

Raspberry Pi에서 Arduino로 데이터를 보낼 때도 마지막에 \n을 붙여야 합니다.

예:

PING_FROM_PYTHON:1\n

Arduino는 \n을 보고 다음처럼 판단합니다.

명령 하나가 끝났다.
이제 처리하면 된다.

millis()를 쓰는 이유

delay(1000)을 쓰면 간단하지만 기다리는 동안 다른 처리가 막힙니다.

millis()를 쓰면 Arduino가 계속 동작하면서 시간을 확인할 수 있습니다.

이 구조가 나중에 센서, 모터, 버튼, ROS 2 통신으로 확장할 때 더 좋습니다.

9. 윈도우에서 Arduino Uno 보드 통신 테스트

PC에 연결된 상태에서 테스트 코드를 보드에 업로드 하고 우측 상단의 “시리얼 모니터” 버튼을 클릭하여 출력결과를 확인합니다.

위의 그림처럼 정상출력을 확인한 후에 USB 커넥터를 TurtleBot3의 라즈베리 파이 4 USB 커넥터에 연결합니다.

9. Python 시리얼 통신 예제

먼저 Python 시리얼 라이브러리를 설치합니다.

sudo apt update
sudo apt install python3-pip -y
pip3 install pyserial

Python 파일을 만듭니다.

mkdir -p ~/serial_test
cd ~/serial_test
nano serial_python_test.py

아래 코드를 저장합니다.

#!/usr/bin/env python3

import serial
import time

SERIAL_PORT = "/dev/ttyACM1"
BAUD_RATE = 115200


def main():
    print("Python Serial Test Start")
    print(f"Port: {SERIAL_PORT}")
    print(f"Baud rate: {BAUD_RATE}")

    try:
        ser = serial.Serial(
            port=SERIAL_PORT,
            baudrate=BAUD_RATE,
            timeout=1.0
        )
    except serial.SerialException as e:
        print("Failed to open serial port.")
        print(e)
        return

    time.sleep(2.0)

    print("Serial port opened.")

    send_count = 0

    try:
        while True:
            if ser.in_waiting > 0:
                received_data = ser.readline().decode("utf-8", errors="ignore").strip()

                if received_data:
                    print(f"Received from Arduino: {received_data}")

                    send_count += 1
                    message = f"PING_FROM_PYTHON:{send_count}\n"

                    ser.write(message.encode("utf-8"))
                    print(f"Sent to Arduino: {message.strip()}")

            time.sleep(0.05)

    except KeyboardInterrupt:
        print("\nPython serial test stopped by user.")

    finally:
        ser.close()
        print("Serial port closed.")


if __name__ == "__main__":
    main()

실행 권한을 줍니다.

chmod +x serial_python_test.py

실행합니다.

python3 serial_python_test.py

정상 출력 예시:

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

10. Python 코드 설명

1) 첫 줄 설명

#!/usr/bin/env python3

이 줄은 Linux에서 이 파일을 Python 3로 실행하라는 의미입니다.

예를 들어 파일에 실행 권한을 준 뒤,

chmod +x serial_python_test.py

다음처럼 실행할 수 있습니다.

./serial_python_test.py

이때 시스템은 첫 줄을 보고 Python 3 인터프리터로 이 파일을 실행합니다.

단순히 아래처럼 실행할 경우에는 이 줄이 없어도 됩니다.

python3 serial_python_test.py

2) 라이브러리 불러오기

import serial
import time

Python에서 외부 기능을 사용하려면 import로 라이브러리를 불러옵니다.

serial
import serial

serial은 Python에서 시리얼 통신을 하기 위한 라이브러리입니다.

정확히는 pyserial 패키지에서 제공하는 모듈입니다.

설치 명령은 다음과 같습니다.

pip3 install pyserial

이 라이브러리를 사용하면 /dev/ttyACM0, /dev/ttyUSB0 같은 시리얼 장치와 데이터를 주고받을 수 있습니다.

time
import time

time 라이브러리는 시간 지연을 만들 때 사용합니다.

이 코드에서는 Arduino가 리셋되고 준비될 시간을 주기 위해 사용합니다.

time.sleep(2.0)

3) 시리얼 포트와 통신 속도 설정

SERIAL_PORT = "/dev/ttyACM1"
BAUD_RATE = 115200

이 두 줄은 시리얼 통신의 기본 설정입니다.

SERIAL_PORT
SERIAL_PORT = "/dev/ttyACM1"

Arduino Uno가 Raspberry Pi에 USB로 연결되면 보통 다음 장치 파일로 인식됩니다.

/dev/ttyACM1

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

그래서 Python은 이 경로를 열어서 Arduino와 통신합니다.

만약 Arduino가 /dev/ttyUSB0로 잡혔다면 다음처럼 수정해야 합니다.

SERIAL_PORT = "/dev/ttyUSB0"

현재 연결된 시리얼 장치는 다음 명령으로 확인할 수 있습니다.

ls /dev/ttyACM*

또는

ls /dev/ttyUSB*

더 자세히 보려면 다음 명령도 사용할 수 있습니다.

dmesg | grep tty
BAUD_RATE
BAUD_RATE = 115200

BAUD_RATE는 시리얼 통신 속도입니다.

Arduino 코드에도 다음과 같은 줄이 있었습니다.

Serial.begin(115200);

Python과 Arduino의 속도는 반드시 같아야 합니다.

Arduino Uno: 115200
Python: 115200

만약 한쪽은 115200이고 다른 쪽은 9600이면 정상적으로 통신되지 않습니다.

문자가 깨지거나 아무 데이터도 안 보일 수 있습니다.

4) main() 함수

def main():

Python에서 def는 함수를 정의할 때 사용합니다.

여기서는 전체 시리얼 통신 동작을 main() 함수 안에 넣었습니다.

이렇게 하면 코드 구조가 깔끔해집니다.

프로그램이 하는 주요 일은 전부 main() 안에서 실행됩니다.

main()
├─ 시리얼 포트 정보 출력
├─ 시리얼 포트 열기
├─ Arduino 데이터 수신
├─ Arduino로 데이터 송신
└─ 종료 시 포트 닫기

5) 시작 메시지 출력

print("Python Serial Test Start")
print(f"Port: {SERIAL_PORT}")
print(f"Baud rate: {BAUD_RATE}")

이 부분은 프로그램이 시작되었음을 터미널에 출력합니다.

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

Python Serial Test Start
Port: /dev/ttyACM0
Baud rate: 115200

이 출력은 단순하지만 강의에서 중요합니다.

수강생이 현재 어떤 포트와 통신 속도를 사용하고 있는지 바로 확인할 수 있기 때문입니다.

f-string 설명
print(f"Port: {SERIAL_PORT}")

Python의 f-string 문법입니다.

문자열 앞에 f를 붙이면 {} 안에 변수 값을 넣을 수 있습니다.

예를 들어,

SERIAL_PORT = "/dev/ttyACM0"

이면 출력은 다음과 같습니다.

Port: /dev/ttyACM0

6) 시리얼 포트 열기

try:
ser = serial.Serial(
port=SERIAL_PORT,
baudrate=BAUD_RATE,
timeout=1.0
)
except serial.SerialException as e:
print("Failed to open serial port.")
print(e)
return

이 부분은 Arduino와 연결된 시리얼 포트를 여는 코드입니다.

try-except를 사용하는 이유

시리얼 포트를 열 때는 실패할 가능성이 있습니다.

예를 들어 다음과 같은 문제가 생길 수 있습니다.

1. Arduino가 연결되어 있지 않음
2. 포트 이름이 틀림
3. 권한이 없음
4. 다른 프로그램이 이미 포트를 사용 중

그래서 try-except를 사용합니다.

시리얼 포트를 정상적으로 열면 try 안의 코드가 실행됩니다.

문제가 생기면 except 쪽으로 넘어갑니다.

정상적으로 열리는 경우
ser = serial.Serial(
port=SERIAL_PORT,
baudrate=BAUD_RATE,
timeout=1.0
)

이 코드가 성공하면 ser라는 객체가 만들어집니다.

이후부터는 ser를 사용해서 Arduino와 데이터를 주고받습니다.

ser.readline() → Arduino에서 데이터 읽기
ser.write() → Arduino로 데이터 보내기
ser.close() → 시리얼 포트 닫기
포트 설정 설명
port=SERIAL_PORT

사용할 시리얼 포트입니다.

현재 코드에서는 다음 값입니다.

/dev/ttyACM1
baudrate=BAUD_RATE

통신 속도입니다.

현재 코드에서는 다음 값입니다.

115200
timeout=1.0

데이터를 읽을 때 최대 1초까지 기다리겠다는 의미입니다.

만약 데이터가 없으면 무한정 기다리지 않고 1초 후 빠져나옵니다.

실패하는 경우
except serial.SerialException as e:
print("Failed to open serial port.")
print(e)
return

시리얼 포트를 열지 못하면 에러 메시지를 출력하고 main() 함수를 종료합니다.

예를 들어 권한 문제가 있으면 다음과 비슷한 메시지가 나올 수 있습니다.

Failed to open serial port.
[Errno 13] could not open port /dev/ttyACM1: Permission denied

이 경우에는 다음 명령으로 권한을 해결합니다.

sudo usermod -a -G dialout $USER
sudo reboot

7) Arduino 준비 대기

time.sleep(2.0)

이 코드는 2초 동안 기다립니다.

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

즉, Python 코드가 /dev/ttyACM1를 여는 순간 Arduino가 다시 시작될 수 있습니다.

Arduino가 리셋되면 바로 통신할 수 있는 상태가 아닙니다.
부팅하고 setup()을 실행할 시간이 필요합니다.

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

Python이 시리얼 포트 열기

Arduino Uno 자동 리셋

Arduino setup() 실행

2초 대기

본격적인 통신 시작

이 대기 시간이 없으면 첫 번째 메시지인 ARDUINO_READY를 놓칠 수 있습니다.

8) 포트 열림 메시지

print("Serial port opened.")

시리얼 포트가 정상적으로 열리고 2초 대기까지 끝났다는 뜻입니다.

터미널에 다음처럼 보입니다.

Serial port opened.

이 메시지가 보이면 Python 프로그램이 Arduino와 통신할 준비가 된 것입니다.

9) 송신 카운트 변수

send_count = 0

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

처음에는 0입니다.

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

0 → 1 → 2 → 3 → 4

그리고 이 숫자를 메시지에 붙여 보냅니다.

PING_FROM_PYTHON:1
PING_FROM_PYTHON:2
PING_FROM_PYTHON:3

이렇게 하면 강의 중에 몇 번째 송신인지 확인하기 쉽습니다.

10) 무한 반복 통신 시작

try:
while True:

이 부분부터 실제 시리얼 통신이 계속 반복됩니다.

while True는 무한 반복입니다.

즉, 사용자가 Ctrl + C를 누르기 전까지 계속 실행됩니다.

이 안에서 Python은 계속 다음 일을 반복합니다.

1. Arduino에서 들어온 데이터가 있는지 확인
2. 데이터가 있으면 한 줄 읽기
3. 터미널에 출력
4. Arduino로 응답 문자열 보내기
5. 잠깐 쉬기

11) 수신 데이터가 있는지 확인

if ser.in_waiting > 0:

ser.in_waiting은 현재 시리얼 수신 버퍼에 들어와 있는 데이터의 바이트 수를 의미합니다.

쉽게 말하면,

Arduino에서 받은 데이터가 있는가?

를 확인하는 코드입니다.

예를 들어 받을 데이터가 없으면 다음과 같습니다.

ser.in_waiting = 0

데이터가 있으면 1보다 큰 값이 됩니다.

ser.in_waiting = 18

그래서 다음 조건을 사용합니다.

if ser.in_waiting > 0:

뜻은 다음과 같습니다.

읽을 데이터가 1바이트 이상 있으면 데이터를 읽어라.

12) Arduino 데이터 한 줄 읽기

received_data = ser.readline().decode("utf-8", errors="ignore").strip()

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

조금 나누어서 보면 다음과 같습니다.

ser.readline()

시리얼 포트에서 한 줄을 읽습니다.

Arduino는 Serial.println()으로 데이터를 보내고 있습니다.

예:

Serial.println("ARDUINO_COUNT:1");

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

ARDUINO_COUNT:1\n

Python의 readline()은 이 \n을 만날 때까지 데이터를 읽습니다.

decode("utf-8", errors="ignore")
.decode("utf-8", errors="ignore")

시리얼로 들어온 데이터는 처음에는 바이트 데이터입니다.

예를 들어 내부적으로는 이런 형태입니다.

b'ARDUINO_COUNT:1\r\n'

사람이 읽을 수 있는 문자열로 바꾸려면 디코딩이 필요합니다.

decode("utf-8")

errors="ignore"는 혹시 깨진 문자가 들어오더라도 에러를 내지 않고 무시하라는 의미입니다.

시리얼 통신에서는 가끔 깨진 바이트가 들어올 수 있으므로 안전하게 넣어둔 옵션입니다.

strip()
.strip()

문자열 앞뒤의 공백, 줄바꿈 문자를 제거합니다.

Arduino에서 들어온 원본 데이터가 다음과 같다면,

ARDUINO_COUNT:1\r\n

strip() 후에는 다음처럼 됩니다.

ARDUINO_COUNT:1

그래서 터미널에 깔끔하게 출력할 수 있습니다.

13) 빈 데이터인지 확인

if received_data:

이 조건은 received_data가 비어 있지 않을 때만 실행됩니다.

즉, 실제로 읽은 문자열이 있을 때만 처리합니다.

빈 문자열이면 아무것도 하지 않습니다.

예를 들어 다음은 처리합니다.

ARDUINO_COUNT:1

하지만 다음은 처리하지 않습니다.

""

이 조건을 넣으면 불필요한 빈 줄 출력이나 빈 메시지 송신을 막을 수 있습니다.

14) 받은 데이터 출력

print(f"Received from Arduino: {received_data}")

Arduino에서 받은 데이터를 Raspberry Pi 터미널에 출력합니다.

예를 들어 Arduino에서 다음 데이터를 보냈다면,

ARDUINO_COUNT:1

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

Received from Arduino: ARDUINO_COUNT:1

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

Arduino가 데이터를 보냈다.
USB 시리얼 통신이 동작한다.
Raspberry Pi Python 코드가 데이터를 받았다.

15) 송신 카운트 증가

send_count += 1

Arduino로 응답을 보내기 전에 송신 카운트를 1 증가시킵니다.

처음에는 send_count = 0이었습니다.

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

send_count = 1

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

send_count = 2

이 값을 이용해서 메시지를 만듭니다.

16) Arduino로 보낼 메시지 만들기

message = f"PING_FROM_PYTHON:{send_count}\n"

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

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

PING_FROM_PYTHON:1\n

여기서 중요한 것은 마지막의 \n입니다.

Arduino 코드는 \n을 기준으로 한 줄의 명령이 끝났다고 판단합니다.

Arduino 코드에는 다음 부분이 있었습니다.

if (receivedChar == '\n')

즉, Python이 \n을 보내지 않으면 Arduino는 다음처럼 생각합니다.

아직 한 줄이 끝나지 않았다.
계속 기다려야 한다.

그래서 반드시 메시지 끝에 \n을 붙여야 합니다.

message = f"PING_FROM_PYTHON:{send_count}\n"

이 부분이 중요합니다.

17) Arduino로 데이터 보내기

ser.write(message.encode("utf-8"))

이 줄은 Python에서 Arduino로 데이터를 전송하는 코드입니다.

message는 Python 문자열입니다.

PING_FROM_PYTHON:1\n

하지만 시리얼 포트로 데이터를 보낼 때는 문자열이 아니라 바이트 형태로 보내야 합니다.

그래서 다음과 같이 인코딩합니다.

message.encode("utf-8")

그러면 문자열이 바이트 데이터로 바뀝니다.

b'PING_FROM_PYTHON:1\n'

그 후 ser.write()가 실제로 시리얼 포트로 데이터를 보냅니다.

Arduino는 이 데이터를 받고 LED를 깜빡입니다.

18) 보낸 데이터 출력

print(f"Sent to Arduino: {message.strip()}")

Python이 Arduino로 어떤 데이터를 보냈는지 터미널에 출력합니다.

message에는 마지막에 \n이 들어 있습니다.

PING_FROM_PYTHON:1\n

그대로 출력하면 줄바꿈이 한 번 더 들어가서 보기 불편할 수 있습니다.

그래서 출력할 때는 strip()을 사용합니다.

message.strip()

출력 예시는 다음과 같습니다.

Sent to Arduino: PING_FROM_PYTHON:1

19) 반복 주기 조절

time.sleep(0.05)

반복문이 너무 빠르게 돌지 않도록 0.05초 쉬게 합니다.

0.05초 = 50ms

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

시리얼 통신 테스트에서는 50ms 정도 쉬어도 충분합니다.

20. Ctrl+C 종료 처리

except KeyboardInterrupt:
print("\nPython serial test stopped by user.")

터미널에서 Python 프로그램을 실행한 뒤 멈추려면 보통 Ctrl + C를 누릅니다.

이때 Python에서는 KeyboardInterrupt 예외가 발생합니다.

이 코드는 사용자가 Ctrl + C를 눌렀을 때 프로그램이 갑자기 에러처럼 종료되지 않고, 친절하게 종료 메시지를 출력하게 합니다.

출력 예:

Python serial test stopped by user.

강의 중에는 이 부분을 이렇게 설명하면 됩니다.

무한 반복 프로그램은 Ctrl+C로 종료합니다.
이 코드는 Ctrl+C가 눌렸을 때 안전하게 종료되도록 처리한 부분입니다.

21) 시리얼 포트 닫기

finally:
ser.close()
print("Serial port closed.")

finally는 예외가 발생하든 안 하든 마지막에 반드시 실행되는 부분입니다.

여기서는 시리얼 포트를 닫습니다.

ser.close()

시리얼 포트를 열었으면 프로그램 종료 전에 닫아주는 것이 좋습니다.

닫지 않고 종료되면 다음 실행 때 포트가 꼬이거나, 다른 프로그램에서 포트를 열기 어려운 상황이 생길 수 있습니다.

물론 운영체제가 대부분 정리해주긴 하지만, 강의용 코드에서는 명확히 닫아주는 습관이 좋습니다.

출력:

Serial port closed.

22) 프로그램 시작 지점

if __name__ == "__main__":
main()

이 부분은 Python 프로그램의 시작점을 정하는 코드입니다.

현재 파일을 직접 실행하면 main() 함수가 실행됩니다.

예를 들어 다음처럼 실행하면,

python3 serial_python_test.py

Python은 아래 조건을 만족합니다.

__name__ == "__main__"

그래서 main()이 실행됩니다.

이 구조를 쓰면 나중에 이 파일을 다른 Python 파일에서 import할 때 자동 실행되는 것을 막을 수 있습니다.

강의에서는 간단히 이렇게 설명하면 됩니다.

이 파일을 직접 실행했을 때 main() 함수를 시작하라는 의미입니다.

23) 실행 결과 예시

Arduino Uno가 정상적으로 연결되어 있고, Arduino 코드가 업로드되어 있다면 Python 실행 결과는 다음과 비슷합니다.

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

이 출력에서 중요한 부분은 다음입니다.

Received from Arduino

이 문장이 보이면 Raspberry Pi가 Arduino 데이터를 받은 것입니다.

Sent to Arduino

이 문장이 보이면 Raspberry Pi가 Arduino로 데이터를 보낸 것입니다.

그리고 Arduino 보드의 내장 LED가 깜빡이면 Arduino가 Python 데이터를 받은 것입니다.

10. Python 소스 테스트

1) Python 시리얼 라이브러리 설치

Python에서 시리얼 통신을 하려면 pyserial이 필요합니다.

sudo apt update
sudo apt install python3-pip -y
pip3 install pyserial

설치 확인은 다음 명령으로 할 수 있습니다.

python3 -m serial.tools.list_ports

Arduino가 연결되어 있다면 다음과 비슷하게 출력됩니다.

/dev/ttyACM0        
/dev/ttyACM1        
/dev/ttyAMA0        
/dev/ttyUSB0        
4 ports found

2) 실행 권한 부여

chmod +x serial_python_test.py

3) Python 코드 실행

python3 serial_python_test.py

또는 실행 권한을 줬다면 다음처럼 실행할 수도 있습니다.

./serial_python_test.py

4) 정상 실행 결과 확인

정상적으로 동작하면 Raspberry Pi 터미널에 다음과 비슷한 내용이 출력됩니다.

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

이때 Arduino Uno 보드의 내장 LED가 깜빡여야 합니다.

LED가 깜빡인다는 것은 다음 의미입니다.

Raspberry Pi에서 Arduino로 데이터가 전송되었다.
Arduino가 데이터를 받았다.
Arduino 코드에서 blinkLed() 함수가 실행되었다.

5) Python 테스트 종료

테스트를 멈추려면 터미널에서 다음 키를 누릅니다.

Ctrl + C

정상 종료 메시지는 다음과 비슷합니다.

Python serial test stopped by user.
Serial port closed.

Leave a Comment