1. 목표

더보기

(1) 전체 시스템 구조와 학습 목표

  • 아날로그 입력(0~1023)을 이해하고 읽을 수 있다.
  • map() 함수를 사용해 아날로그 값을 PWM 범위(0~255)로 변환할 수 있다.
  • 슬라이드 스위치의 디지털 입력(HIGH/LOW)을 조건문으로 처리할 수 있다.
  • PWM 핀을 이용해 DC 모터의 속도를 제어할 수 있다.
  • 디지털 입력을 이용해 DC 모터의 회전 방향을 제어할 수 있다.
  • PySide6에서 시리얼 데이터를 QThread 기반 비동기 수신으로 처리할 수 있다.
  • TX / RX 로그를 통해 전체 제어 흐름을 이해할 수 있다.

 

 

 

 

(2) 최종 결과물

PySide6 GUI 프로그램

  • 시리얼 포트 연결/해제
  • 실시간 RX 로그 출력

Arduino

  • 가변저항 값에 따라 DC 모터 속도 변경
  • 슬라이드 스위치 위치에 따라 모터 방향 변경

시리얼 통신을 통한 상태 모니터링

  • A0 값
  • PWM 값
  • 회전 방향(CW / CCW)

 

2. 아두이노 스케치 구현 (DC 모터 제어)

: 교재 P.233~ 238

더보기

(1) 아두이노 동작 구조

입력 → 처리 → 출력 구조

  • 입력
    • A0: 가변저항 (아날로그 입력)
    • D8: 슬라이드 스위치 (디지털 입력)
  • 처리
    • analogRead()로 입력값 수집
    • map()으로 PWM 범위 변환
    • if / else 조건문으로 방향 결정
  • 출력
    • analogWrite()로 모터 속도 제어
    • PWM 핀 전환으로 방향 제어
    • Serial.println()으로 상태 출력

 

 

 

(2) 아두이노 측 회로 기본 회로 구성

  • 가변저항
    • 양 끝: 5V / GND
    • 가운데: A0
  • 슬라이드 스위치
    • 한쪽: 5V
    • 한쪽: GND
    • 가운데: D8
  • DC 모터
    • PWM 핀: D9, D10
    • 모터 드라이버 사용 권장

⚠️ 주의
DC 모터는 LED와 달리 많은 전류를 요구합니다.
실제 보드에서는 L293D / L298N / TB6612FNG와 같은 모터 드라이버를 반드시 사용하는 것이 안전합니다.

 

 

(3) 아두이노 스케치 코드

// DC Motor control with potentiometer and slide switch
// A0  : potentiometer (speed)
// D8  : slide switch (direction)
// D9  : PWM motor pin 1
// D10 : PWM motor pin 2

const int MOTOR_PIN_1 = 9;
const int MOTOR_PIN_2 = 10;
const int SWITCH_PIN  = 8;
const int POT_PIN     = A0;

void setup() {
  pinMode(MOTOR_PIN_1, OUTPUT);
  pinMode(MOTOR_PIN_2, OUTPUT);
  pinMode(SWITCH_PIN, INPUT);

  Serial.begin(9600);
  Serial.println("READY");
}

void loop() {
  int inputValue = analogRead(POT_PIN);              // 0~1023
  int pwmValue   = map(inputValue, 0, 1023, 0, 255); // 0~255
  int dirSwitch  = digitalRead(SWITCH_PIN);

  if (dirSwitch == LOW) {
    analogWrite(MOTOR_PIN_1, pwmValue);
    analogWrite(MOTOR_PIN_2, 0);
    Serial.print("A0=");
    Serial.print(inputValue);
    Serial.print(",PWM=");
    Serial.print(pwmValue);
    Serial.println(",DIR=CW");
  } else {
    analogWrite(MOTOR_PIN_1, 0);
    analogWrite(MOTOR_PIN_2, pwmValue);
    Serial.print("A0=");
    Serial.print(inputValue);
    Serial.print(",PWM=");
    Serial.print(pwmValue);
    Serial.println(",DIR=CCW");
  }

  delay(100);
}

 

 

(4) 시리얼 모니터 테스트

 

3. PySide6 GUI 구현

더보기

(1) GUI 구현 목표

 

 

 

 

(2) 소스 코드

import re
import sys
import time
from dataclasses import dataclass

import serial
from serial.tools import list_ports

from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import (
    QApplication,
    QComboBox,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)


@dataclass
class SerialConfig:
    port: str
    baudrate: int


class SerialReaderThread(QThread):
    received = Signal(str)
    status = Signal(str)

    def __init__(self, ser: serial.Serial):
        super().__init__()
        self._ser = ser
        self._running = True

    def stop(self):
        self._running = False

    def run(self):
        self.status.emit("RX thread started")
        try:
            while self._running:
                if self._ser is None or not self._ser.is_open:
                    time.sleep(0.05)
                    continue

                try:
                    line = self._ser.readline()
                    if line:
                        text = line.decode(errors="replace").rstrip("\r\n")
                        self.received.emit(text)
                    else:
                        time.sleep(0.01)
                except serial.SerialException as e:
                    self.status.emit(f"SerialException: {e}")
                    time.sleep(0.2)
                except Exception as e:
                    self.status.emit(f"RX error: {e}")
                    time.sleep(0.2)
        finally:
            self.status.emit("RX thread stopped")


class MainWindow(QMainWindow):
    # Arduino output example:
    # "A0=512,PWM=127,DIR=CW"
    LINE_RE = re.compile(r"^A0=(\d+),PWM=(\d+),DIR=(CW|CCW)$")

    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 Arduino DC Motor Monitor")
        self.resize(860, 560)

        self.ser: serial.Serial | None = None
        self.rx_thread: SerialReaderThread | None = None

        # ---- Connection UI
        self.combo_port = QComboBox()
        self.btn_refresh = QPushButton("Refresh")

        self.combo_baud = QComboBox()
        self.combo_baud.addItems(["9600", "115200"])

        self.btn_open = QPushButton("Open")
        self.btn_close = QPushButton("Close")
        self.btn_close.setEnabled(False)

        # ---- Status UI
        self.lbl_a0 = QLabel("A0: -")
        self.lbl_pwm = QLabel("PWM: -")
        self.lbl_dir = QLabel("DIR: -")
        self.btn_dir_badge = QPushButton("MOTOR: -")
        self.btn_dir_badge.setEnabled(False)
        self.btn_dir_badge.setMinimumHeight(44)

        status_group = QGroupBox("Live Status (Parsed from RX)")
        status_row = QHBoxLayout()
        status_row.addWidget(self.lbl_a0)
        status_row.addWidget(self.lbl_pwm)
        status_row.addWidget(self.lbl_dir)
        status_row.addStretch(1)
        status_row.addWidget(self.btn_dir_badge, 1)
        status_group.setLayout(status_row)

        # ---- Send test UI (optional)
        self.edit_send = QLineEdit()
        self.edit_send.setPlaceholderText("Send Data (optional test)")
        self.btn_send = QPushButton("Send")
        self.btn_send.setEnabled(False)

        send_row = QHBoxLayout()
        send_row.addWidget(self.edit_send, 1)
        send_row.addWidget(self.btn_send)

        # ---- Log UI
        self.log = QTextEdit()
        self.log.setReadOnly(True)

        # ---- Layout root
        top_row = QHBoxLayout()
        top_row.addWidget(QLabel("Port"))
        top_row.addWidget(self.combo_port, 1)
        top_row.addWidget(self.btn_refresh)
        top_row.addSpacing(8)
        top_row.addWidget(QLabel("Baudrate"))
        top_row.addWidget(self.combo_baud)
        top_row.addSpacing(8)
        top_row.addWidget(self.btn_open)
        top_row.addWidget(self.btn_close)

        root = QVBoxLayout()
        root.addLayout(top_row)
        root.addWidget(status_group)
        root.addLayout(send_row)
        root.addWidget(self.log, 1)

        central = QWidget()
        central.setLayout(root)
        self.setCentralWidget(central)

        # ---- Signals
        self.btn_refresh.clicked.connect(self.refresh_ports)
        self.btn_open.clicked.connect(self.open_serial)
        self.btn_close.clicked.connect(self.close_serial)
        self.btn_send.clicked.connect(self.send_text)
        self.edit_send.returnPressed.connect(self.send_text)

        # ---- Init
        self.refresh_ports()

    def append_log(self, text: str):
        self.log.append(text)

    def refresh_ports(self):
        self.combo_port.clear()
        ports = list_ports.comports()
        for p in ports:
            self.combo_port.addItem(f"{p.device} - {p.description}", p.device)

        if self.combo_port.count() == 0:
            self.combo_port.addItem("(no ports)", "")
        self.append_log("Ports refreshed")

    def get_config(self) -> SerialConfig:
        return SerialConfig(
            port=str(self.combo_port.currentData() or ""),
            baudrate=int(self.combo_baud.currentText()),
        )

    def set_connected_ui(self, connected: bool):
        self.btn_open.setEnabled(not connected)
        self.btn_close.setEnabled(connected)
        self.btn_send.setEnabled(connected)
        self.btn_dir_badge.setEnabled(connected)

        self.combo_port.setEnabled(not connected)
        self.combo_baud.setEnabled(not connected)
        self.btn_refresh.setEnabled(not connected)

    def open_serial(self):
        cfg = self.get_config()
        if not cfg.port:
            QMessageBox.warning(self, "Warning", "No serial port selected.")
            return

        try:
            self.ser = serial.Serial(
                port=cfg.port,
                baudrate=cfg.baudrate,
                timeout=0.1,
                write_timeout=0.5,
            )
        except Exception as e:
            QMessageBox.critical(self, "Open failed", str(e))
            self.ser = None
            return

        self.append_log(f"Opened: {cfg.port} @ {cfg.baudrate}")

        self.rx_thread = SerialReaderThread(self.ser)
        self.rx_thread.received.connect(self.on_rx_line)
        self.rx_thread.status.connect(lambda s: self.append_log(f"[SYS] {s}"))
        self.rx_thread.start()

        self.set_connected_ui(True)

    def close_serial(self):
        if self.rx_thread is not None:
            self.rx_thread.stop()
            self.rx_thread.wait(800)
            self.rx_thread = None

        if self.ser is not None:
            try:
                if self.ser.is_open:
                    self.ser.close()
            except Exception:
                pass
            self.ser = None

        self.append_log("Closed")
        self.set_connected_ui(False)

        # reset status
        self.lbl_a0.setText("A0: -")
        self.lbl_pwm.setText("PWM: -")
        self.lbl_dir.setText("DIR: -")
        self.btn_dir_badge.setText("MOTOR: -")

    def write_line(self, text: str):
        if self.ser is None or not self.ser.is_open:
            self.append_log("[ERR] Serial not open")
            return

        try:
            data = (text + "\n").encode("utf-8")
            self.ser.write(data)
            self.ser.flush()
            self.append_log(f"TX >> {text}")
        except Exception as e:
            self.append_log(f"[ERR] TX failed: {e}")

    def send_text(self):
        text = self.edit_send.text().strip()
        if not text:
            return
        self.write_line(text)
        self.edit_send.clear()

    def on_rx_line(self, line: str):
        self.append_log(f"RX << {line}")

        # Ignore the READY line but keep it in the log
        if line == "READY":
            return

        m = self.LINE_RE.match(line)
        if not m:
            return

        a0 = int(m.group(1))
        pwm = int(m.group(2))
        direction = m.group(3)

        self.lbl_a0.setText(f"A0: {a0}")
        self.lbl_pwm.setText(f"PWM: {pwm}")
        self.lbl_dir.setText(f"DIR: {direction}")
        self.btn_dir_badge.setText(f"MOTOR: {direction}")

    def closeEvent(self, event):
        self.close_serial()
        super().closeEvent(event)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec())

 

ㄴ 3.1 RX (수신) 구현 - QThread 사용

더보기


왜 QThread가 필요한가?

  • 시리얼 수신은 블로킹 작업
  • GUI 응답성 유지 필요

 

학습 포인트

  • 스레드 → UI 직접 접근 금지
  • Signal / Slot 구조 필수

ㄴ 3.2 TX & RX

더보기

(1) TR & TX 기본 의미

Beginner-friendly educational illustration explaining serial communication using a delivery analogy.

Left: a computer labeled "PySide6 App".
Right: an Arduino board labeled "Arduino".

Between them: a USB cable visualized as a road.

Data packets shown as envelopes traveling both directions:
- Envelope labeled "1" going to Arduino
- Envelope labeled "LED:ON" returning to PC

Simple captions:
- "Send data (TX)"
- "Receive data (RX)"

Style:
- Friendly flat illustration
- Soft colors
- Cartoon-like but professional
- Ideal for beginners

Aspect ratio: 16:9
Clean background, no realistic textures

TX / RX는 시리얼 통신, 네트워크, 임베디드 전반에서 사용되는 데이터 송수신 방향 표기입니다.

 

 

(2) 용어 개념

용어 약어 의미 설명
TX Transmit 송신 장치에서 밖으로 내보내는 데이터
RX Receive 수신 장치로 들어오는 데이터
  • TX: 내가 보낸다
  • RX: 내가 받는다
  • TX/RX는 절대적 개념이 아니다
    •  항상 "누구 기준인지" 먼저 생각한다
    • 기준 장치가 바뀌면 의미가 바뀜
  • 핀 이름과 데이터 흐름을 혼동
    • TX 핀은 "여기서 나간다"
    • TX >>   /   RX <<  처럼 방향을 명확히 표기하는 것이 좋음

 

4. 테스트

더보기
Flat educational diagram illustrating Arduino serial control logic.

Center: Arduino Uno board icon.

Incoming serial messages shown as text bubbles:
- "1"
- "0"
- "Hello"

Flowchart-style logic:
IF message == "1" → LED ON (Pin 13 HIGH)
IF message == "0" → LED OFF (Pin 13 LOW)
ELSE → Serial print "ECHO: message"

LED icon lights up when ON and turns gray when OFF.

Style:
- Simple flowchart infographic
- Rounded boxes and arrows
- Beginner-friendly coding education tone
- Flat vector style
- Clear labels in English
High clarity, no background clutter

 

  1. Arduino 스케치 업로드
  2. PySide6 프로그램 실행
  3. COM 포트 선택 → Open
  4. 가변저항 회전
    • PWM 값 변화 확인
  5. 슬라이드 스위치 전환
    • DIR 값(CW / CCW) 변경 확인
  6. RX 로그로 전체 흐름 확인

 

 

 

실습 과제 (확장)

더보기

기본 과제

  • 가변저항 값에 따라 DC 모터 속도 변화 확인
  • 슬라이드 스위치로 회전 방향 제어
  • PySide6 RX 로그 정상 출력

확장 과제

  • PySide6 슬라이더로 PWM 직접 제어
  • 방향 토글 버튼 추가
  • 모터 속도에 따라 UI 색상 변화
  • CSV 파일로 로그 저장

다음 단계 제안

  • 센서 데이터 실시간 그래프 시각화
  • 다중 모터 제어 구조
  • 명령 프로토콜 설계 (CMD:VALUE)
  • IoT · 로보틱스 융합 프로젝트 확장