프로젝트 16: 시리얼 통신

프로젝트 16: 시리얼 통신

UART 시리얼 통신을 이용한 PC와의 양방향 데이터 교환을 배웁니다.

학습 목표

  • UART 통신 원리 이해
  • 시리얼 데이터 송수신
  • 문자열 파싱
  • 명령어 인터프리터 구현
  • 디버깅 기법

사전 지식

  • Arduino 기본 구조
  • 문자열 처리
  • GPIO 제어

1. UART 통신 개념

UART란?

UART (Universal Asynchronous Receiver/Transmitter)는 가장 기본적인 시리얼 통신 방식입니다.

UART 통신 구조:

Arduino                           PC
┌─────────┐                    ┌─────────┐
│         │───── TX ─────────→│ RX      │
│  MCU    │                    │  USB    │
│         │←───── RX ─────────│ TX      │
│         │───── GND ─────────│ GND     │
└─────────┘                    └─────────┘

- TX (Transmit): 데이터 송신
- RX (Receive): 데이터 수신
- 비동기 통신: 클럭 신호 없이 약속된 속도로 통신

통신 파라미터

Baud Rate (보레이트):
- 초당 전송 비트 수
- 일반적인 값: 9600, 19200, 38400, 57600, 115200
- 송신측과 수신측이 같아야 함

데이터 프레임:
┌─────┬────────────┬────────┬─────┐
│Start│  Data bits │ Parity │Stop │
│ bit │ (5-9 bits) │ (opt)  │bit  │
└─────┴────────────┴────────┴─────┘

일반적인 설정: 8N1
- 8 데이터 비트
- N (No parity): 패리티 없음
- 1 스톱 비트

9600 Baud 전송 예시

문자 'A' (ASCII 65 = 0b01000001) 전송:

HIGH ─────┐     ┌─────┐                 ┌─────────
          │     │     │                 │
LOW       └─────┘     └─────────────────┘
          │Start│  0  1  0  0  0  0  0  1 │ Stop │
          │ bit │      Data bits (LSB first)│ bit │
                        ← 'A' = 0x41 →

시간: 1/9600 ≈ 104μs per bit
1 문자 = 10 bits = 약 1.04ms
초당 최대 약 960 문자 전송 가능

2. Arduino Serial 라이브러리

기본 함수

// 시리얼 초기화
Serial.begin(baudrate);      // 9600, 115200 등
Serial.begin(9600, config);  // 설정 포함 (SERIAL_8N1 등)

// 송신 (출력)
Serial.print(data);          // 줄바꿈 없이
Serial.println(data);        // 줄바꿈 포함
Serial.write(byte);          // 1바이트 전송
Serial.write(buffer, len);   // 버퍼 전송

// 수신 (입력)
Serial.available();          // 수신 대기 중인 바이트 수
Serial.read();               // 1바이트 읽기 (-1 if empty)
Serial.peek();               // 읽지 않고 확인
Serial.readBytes(buf, len);  // 여러 바이트 읽기
Serial.readString();         // 문자열로 읽기
Serial.readStringUntil(ch);  // 특정 문자까지 읽기

// 기타
Serial.flush();              // 송신 완료까지 대기
Serial.setTimeout(ms);       // 타임아웃 설정 (기본 1000ms)

기본 출력

// serial_output.ino
// 다양한 형식의 시리얼 출력

void setup() {
    Serial.begin(9600);

    // 연결 대기 (선택적)
    while (!Serial) {
        ; // USB 연결 대기
    }

    Serial.println("=== Serial Output Demo ===");
}

void loop() {
    // 문자열 출력
    Serial.println("Hello, World!");

    // 숫자 출력
    int num = 42;
    Serial.print("Number: ");
    Serial.println(num);

    // 다양한 진법
    Serial.print("Decimal: ");
    Serial.println(255, DEC);    // 255

    Serial.print("Binary:  ");
    Serial.println(255, BIN);    // 11111111

    Serial.print("Octal:   ");
    Serial.println(255, OCT);    // 377

    Serial.print("Hex:     ");
    Serial.println(255, HEX);    // FF

    // 실수 출력
    float pi = 3.14159;
    Serial.print("Pi: ");
    Serial.println(pi, 4);       // 소수점 4자리

    // 포맷 문자열 (sprintf 사용)
    char buffer[50];
    sprintf(buffer, "x=%d, y=%d, val=%.2f", 10, 20, 3.14);
    Serial.println(buffer);

    delay(3000);
}

기본 입력

// serial_input.ino
// 시리얼 입력 처리

void setup() {
    Serial.begin(9600);
    Serial.println("Type something and press Enter:");
}

void loop() {
    // 수신 데이터가 있는지 확인
    if (Serial.available() > 0) {
        // 한 문자 읽기
        char c = Serial.read();

        // 에코 (받은 문자 그대로 출력)
        Serial.print("Received: '");
        Serial.print(c);
        Serial.print("' (ASCII ");
        Serial.print((int)c);
        Serial.println(")");
    }
}

3. 문자열 수신 처리

줄 단위 읽기

// serial_readline.ino
// 한 줄씩 읽기

String inputString = "";
bool stringComplete = false;

void setup() {
    Serial.begin(9600);
    inputString.reserve(200);  // 메모리 예약
    Serial.println("Enter a line:");
}

void loop() {
    // 한 줄 완성되면 처리
    if (stringComplete) {
        Serial.print("You entered: ");
        Serial.println(inputString);

        // 초기화
        inputString = "";
        stringComplete = false;
    }
}

// 시리얼 이벤트 (자동 호출)
void serialEvent() {
    while (Serial.available()) {
        char c = (char)Serial.read();

        if (c == '\n') {
            stringComplete = true;
        } else if (c != '\r') {  // CR 무시
            inputString += c;
        }
    }
}

char 배열로 읽기 (메모리 효율적)

// serial_char_array.ino
// char 배열 사용 (String 대신)

#define MAX_INPUT 64

char inputBuffer[MAX_INPUT];
int inputIndex = 0;
bool lineReady = false;

void setup() {
    Serial.begin(9600);
    Serial.println("Enter command:");
}

void loop() {
    // 데이터 수신
    while (Serial.available() && !lineReady) {
        char c = Serial.read();

        if (c == '\n' || c == '\r') {
            if (inputIndex > 0) {
                inputBuffer[inputIndex] = '\0';  // 문자열 종료
                lineReady = true;
            }
        } else if (inputIndex < MAX_INPUT - 1) {
            inputBuffer[inputIndex++] = c;
        }
    }

    // 완성된 라인 처리
    if (lineReady) {
        Serial.print("Command: ");
        Serial.println(inputBuffer);

        // 처리 후 초기화
        inputIndex = 0;
        lineReady = false;
    }
}

4. 명령어 파싱

단순 명령어 처리

// serial_commands.ino
// 간단한 명령어 처리

const int LED_PIN = 13;

void setup() {
    Serial.begin(9600);
    pinMode(LED_PIN, OUTPUT);

    Serial.println("=== LED Control ===");
    Serial.println("Commands: ON, OFF, BLINK, STATUS");
}

void processCommand(String cmd) {
    cmd.trim();  // 앞뒤 공백 제거
    cmd.toUpperCase();  // 대문자로 변환

    if (cmd == "ON") {
        digitalWrite(LED_PIN, HIGH);
        Serial.println("LED turned ON");
    }
    else if (cmd == "OFF") {
        digitalWrite(LED_PIN, LOW);
        Serial.println("LED turned OFF");
    }
    else if (cmd == "BLINK") {
        Serial.println("Blinking 5 times...");
        for (int i = 0; i < 5; i++) {
            digitalWrite(LED_PIN, HIGH);
            delay(200);
            digitalWrite(LED_PIN, LOW);
            delay(200);
        }
        Serial.println("Done");
    }
    else if (cmd == "STATUS") {
        Serial.print("LED is ");
        Serial.println(digitalRead(LED_PIN) ? "ON" : "OFF");
    }
    else {
        Serial.print("Unknown command: ");
        Serial.println(cmd);
    }
}

void loop() {
    if (Serial.available()) {
        String input = Serial.readStringUntil('\n');
        processCommand(input);
    }
}

인자가 있는 명령어

// serial_args.ino
// 인자가 있는 명령어 처리

const int LED_PINS[] = {9, 10, 11, 12};
const int NUM_LEDS = 4;

void setup() {
    Serial.begin(9600);
    for (int i = 0; i < NUM_LEDS; i++) {
        pinMode(LED_PINS[i], OUTPUT);
    }

    Serial.println("=== Multi-LED Control ===");
    Serial.println("Commands:");
    Serial.println("  SET <led> <state>  - Set LED 0-3 to 0/1");
    Serial.println("  PATTERN <value>    - Set pattern 0-15");
    Serial.println("  DELAY <ms>         - Set delay time");
    Serial.println("  HELP               - Show this help");
}

void setLED(int led, int state) {
    if (led >= 0 && led < NUM_LEDS) {
        digitalWrite(LED_PINS[led], state ? HIGH : LOW);
        Serial.print("LED ");
        Serial.print(led);
        Serial.print(" set to ");
        Serial.println(state ? "ON" : "OFF");
    } else {
        Serial.println("Invalid LED number (0-3)");
    }
}

void setPattern(int pattern) {
    for (int i = 0; i < NUM_LEDS; i++) {
        digitalWrite(LED_PINS[i], (pattern >> i) & 1);
    }
    Serial.print("Pattern set to ");
    Serial.print(pattern);
    Serial.print(" (0b");
    for (int i = 3; i >= 0; i--) {
        Serial.print((pattern >> i) & 1);
    }
    Serial.println(")");
}

void processCommand(char* input) {
    char* cmd = strtok(input, " ");
    if (cmd == NULL) return;

    // 대문자로 변환
    for (int i = 0; cmd[i]; i++) {
        cmd[i] = toupper(cmd[i]);
    }

    if (strcmp(cmd, "SET") == 0) {
        char* ledStr = strtok(NULL, " ");
        char* stateStr = strtok(NULL, " ");

        if (ledStr && stateStr) {
            int led = atoi(ledStr);
            int state = atoi(stateStr);
            setLED(led, state);
        } else {
            Serial.println("Usage: SET <led> <state>");
        }
    }
    else if (strcmp(cmd, "PATTERN") == 0) {
        char* valStr = strtok(NULL, " ");
        if (valStr) {
            int pattern = atoi(valStr);
            setPattern(pattern & 0x0F);
        } else {
            Serial.println("Usage: PATTERN <0-15>");
        }
    }
    else if (strcmp(cmd, "HELP") == 0) {
        Serial.println("\nCommands:");
        Serial.println("  SET <led> <state>");
        Serial.println("  PATTERN <value>");
        Serial.println("  HELP");
    }
    else {
        Serial.print("Unknown: ");
        Serial.println(cmd);
    }
}

char inputBuffer[64];
int inputIndex = 0;

void loop() {
    while (Serial.available()) {
        char c = Serial.read();

        if (c == '\n' || c == '\r') {
            if (inputIndex > 0) {
                inputBuffer[inputIndex] = '\0';
                processCommand(inputBuffer);
                inputIndex = 0;
            }
        } else if (inputIndex < 63) {
            inputBuffer[inputIndex++] = c;
        }
    }
}

5. 실습: 시리얼 모니터 계산기

// serial_calculator.ino
// 시리얼로 수식을 입력받아 계산

void setup() {
    Serial.begin(9600);

    Serial.println("=================================");
    Serial.println("   Simple Serial Calculator");
    Serial.println("=================================");
    Serial.println("Enter expression (e.g., 10 + 5)");
    Serial.println("Operators: +, -, *, /, %");
    Serial.println("Type 'quit' to exit");
    Serial.println("---------------------------------");
}

float calculate(float a, char op, float b) {
    switch (op) {
        case '+': return a + b;
        case '-': return a - b;
        case '*': return a * b;
        case '/':
            if (b == 0) {
                Serial.println("Error: Division by zero");
                return 0;
            }
            return a / b;
        case '%':
            return (int)a % (int)b;
        default:
            Serial.print("Unknown operator: ");
            Serial.println(op);
            return 0;
    }
}

void processExpression(char* expr) {
    float num1, num2;
    char op;

    // 수식 파싱: "num1 op num2"
    int parsed = sscanf(expr, "%f %c %f", &num1, &op, &num2);

    if (parsed == 3) {
        float result = calculate(num1, op, num2);

        Serial.print(num1);
        Serial.print(" ");
        Serial.print(op);
        Serial.print(" ");
        Serial.print(num2);
        Serial.print(" = ");
        Serial.println(result);
    } else {
        Serial.println("Invalid format. Use: num1 op num2");
    }
}

char inputBuffer[32];
int inputIndex = 0;

void loop() {
    while (Serial.available()) {
        char c = Serial.read();

        if (c == '\n' || c == '\r') {
            if (inputIndex > 0) {
                inputBuffer[inputIndex] = '\0';

                // 종료 명령 확인
                if (strcmp(inputBuffer, "quit") == 0) {
                    Serial.println("Goodbye!");
                    while (1);  // 정지
                }

                processExpression(inputBuffer);
                inputIndex = 0;

                Serial.println("---------------------------------");
            }
        } else if (inputIndex < 31) {
            inputBuffer[inputIndex++] = c;
        }
    }
}

6. 데이터 프로토콜 설계

간단한 프로토콜 예시

// serial_protocol.ino
// 간단한 통신 프로토콜 구현

// 프로토콜 형식:
// <STX><TYPE><LENGTH><DATA><CHECKSUM><ETX>
// STX = 0x02 (Start of Text)
// ETX = 0x03 (End of Text)

#define STX 0x02
#define ETX 0x03

// 메시지 타입
#define MSG_LED_SET     0x01
#define MSG_LED_GET     0x02
#define MSG_TEMP_GET    0x03
#define MSG_ACK         0x10
#define MSG_ERROR       0xFF

const int LED_PIN = 13;

void sendMessage(byte type, byte* data, byte length) {
    byte checksum = type ^ length;
    for (int i = 0; i < length; i++) {
        checksum ^= data[i];
    }

    Serial.write(STX);
    Serial.write(type);
    Serial.write(length);
    Serial.write(data, length);
    Serial.write(checksum);
    Serial.write(ETX);
}

void sendAck() {
    byte data[] = {0x00};
    sendMessage(MSG_ACK, data, 1);
}

void sendError(byte code) {
    byte data[] = {code};
    sendMessage(MSG_ERROR, data, 1);
}

void processMessage(byte type, byte* data, byte length) {
    switch (type) {
        case MSG_LED_SET:
            if (length >= 1) {
                digitalWrite(LED_PIN, data[0] ? HIGH : LOW);
                sendAck();
            } else {
                sendError(0x01);  // Invalid length
            }
            break;

        case MSG_LED_GET:
            {
                byte state = digitalRead(LED_PIN);
                sendMessage(MSG_LED_GET, &state, 1);
            }
            break;

        case MSG_TEMP_GET:
            {
                // 임의의 온도 값 (실제로는 센서에서 읽음)
                byte temp[] = {25, 50};  // 25.50도
                sendMessage(MSG_TEMP_GET, temp, 2);
            }
            break;

        default:
            sendError(0x02);  // Unknown type
    }
}

// 수신 상태 머신
enum RxState { WAIT_STX, WAIT_TYPE, WAIT_LENGTH, WAIT_DATA, WAIT_CHECKSUM, WAIT_ETX };
RxState rxState = WAIT_STX;

byte rxType, rxLength, rxChecksum;
byte rxData[32];
byte rxIndex;

void setup() {
    Serial.begin(9600);
    pinMode(LED_PIN, OUTPUT);
}

void loop() {
    while (Serial.available()) {
        byte b = Serial.read();

        switch (rxState) {
            case WAIT_STX:
                if (b == STX) rxState = WAIT_TYPE;
                break;

            case WAIT_TYPE:
                rxType = b;
                rxChecksum = b;
                rxState = WAIT_LENGTH;
                break;

            case WAIT_LENGTH:
                rxLength = b;
                rxChecksum ^= b;
                rxIndex = 0;
                rxState = (rxLength > 0) ? WAIT_DATA : WAIT_CHECKSUM;
                break;

            case WAIT_DATA:
                rxData[rxIndex++] = b;
                rxChecksum ^= b;
                if (rxIndex >= rxLength) {
                    rxState = WAIT_CHECKSUM;
                }
                break;

            case WAIT_CHECKSUM:
                if (b == rxChecksum) {
                    rxState = WAIT_ETX;
                } else {
                    sendError(0x03);  // Checksum error
                    rxState = WAIT_STX;
                }
                break;

            case WAIT_ETX:
                if (b == ETX) {
                    processMessage(rxType, rxData, rxLength);
                }
                rxState = WAIT_STX;
                break;
        }
    }
}

JSON 형식 사용

// serial_json.ino
// JSON 형식 통신 (ArduinoJson 라이브러리 필요)

// Library Manager에서 "ArduinoJson" 설치

#include <ArduinoJson.h>

const int LED_PIN = 13;
int brightness = 0;
String deviceName = "Arduino";

void setup() {
    Serial.begin(9600);
    pinMode(LED_PIN, OUTPUT);

    Serial.println("{\"status\":\"ready\",\"device\":\"Arduino\"}");
}

void processJson(const char* json) {
    StaticJsonDocument<200> doc;
    DeserializationError error = deserializeJson(doc, json);

    if (error) {
        Serial.print("{\"error\":\"");
        Serial.print(error.c_str());
        Serial.println("\"}");
        return;
    }

    const char* cmd = doc["cmd"];

    if (strcmp(cmd, "set_led") == 0) {
        bool state = doc["state"];
        digitalWrite(LED_PIN, state ? HIGH : LOW);
        Serial.println("{\"result\":\"ok\"}");
    }
    else if (strcmp(cmd, "get_status") == 0) {
        StaticJsonDocument<200> response;
        response["led"] = digitalRead(LED_PIN);
        response["uptime"] = millis() / 1000;
        response["device"] = deviceName;

        serializeJson(response, Serial);
        Serial.println();
    }
    else if (strcmp(cmd, "set_name") == 0) {
        deviceName = doc["name"].as<String>();
        Serial.println("{\"result\":\"ok\"}");
    }
    else {
        Serial.println("{\"error\":\"unknown command\"}");
    }
}

char inputBuffer[256];
int inputIndex = 0;

void loop() {
    while (Serial.available()) {
        char c = Serial.read();

        if (c == '\n') {
            inputBuffer[inputIndex] = '\0';
            if (inputIndex > 0) {
                processJson(inputBuffer);
            }
            inputIndex = 0;
        } else if (inputIndex < 255) {
            inputBuffer[inputIndex++] = c;
        }
    }
}

// 사용 예:
// {"cmd":"set_led","state":true}
// {"cmd":"get_status"}
// {"cmd":"set_name","name":"MyDevice"}

7. 디버깅 기법

디버그 매크로

// debug_macros.ino
// 디버그 출력 매크로

#define DEBUG 1  // 0으로 변경하면 디버그 출력 비활성화

#if DEBUG
    #define DEBUG_PRINT(x)    Serial.print(x)
    #define DEBUG_PRINTLN(x)  Serial.println(x)
    #define DEBUG_PRINTF(...)  { char buf[128]; sprintf(buf, __VA_ARGS__); Serial.print(buf); }
#else
    #define DEBUG_PRINT(x)
    #define DEBUG_PRINTLN(x)
    #define DEBUG_PRINTF(...)
#endif

const int LED_PIN = 13;
const int BUTTON_PIN = 2;

void setup() {
    Serial.begin(9600);
    pinMode(LED_PIN, OUTPUT);
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    DEBUG_PRINTLN("=== Debug Demo ===");
    DEBUG_PRINTF("LED pin: %d\n", LED_PIN);
    DEBUG_PRINTF("Button pin: %d\n", BUTTON_PIN);
}

void loop() {
    static bool lastState = HIGH;
    bool currentState = digitalRead(BUTTON_PIN);

    if (currentState != lastState) {
        DEBUG_PRINTF("Button state changed: %d -> %d\n", lastState, currentState);

        if (currentState == LOW) {
            DEBUG_PRINTLN("Button pressed!");
            digitalWrite(LED_PIN, !digitalRead(LED_PIN));
        }

        lastState = currentState;
    }

    delay(10);
}

변수 모니터링

// serial_monitor.ino
// 실시간 변수 모니터링

const int SENSOR_PIN = A0;
const int LED_PIN = 13;

unsigned long lastPrint = 0;
const unsigned long printInterval = 500;

int sensorValue = 0;
int ledState = LOW;
unsigned long uptime = 0;
int loopCount = 0;

void setup() {
    Serial.begin(115200);  // 빠른 속도 사용
    pinMode(LED_PIN, OUTPUT);

    // CSV 헤더 출력
    Serial.println("time_ms,sensor,led,loop_count");
}

void loop() {
    loopCount++;

    // 센서 읽기
    sensorValue = analogRead(SENSOR_PIN);

    // LED 제어 (센서값에 따라)
    ledState = (sensorValue > 512) ? HIGH : LOW;
    digitalWrite(LED_PIN, ledState);

    // 주기적으로 데이터 출력
    if (millis() - lastPrint >= printInterval) {
        lastPrint = millis();

        // CSV 형식 출력
        Serial.print(millis());
        Serial.print(",");
        Serial.print(sensorValue);
        Serial.print(",");
        Serial.print(ledState);
        Serial.print(",");
        Serial.println(loopCount);

        loopCount = 0;
    }
}

// Serial Plotter에서 그래프로 확인 가능
// Tools → Serial Plotter

상태 머신 디버깅

// state_debug.ino
// 상태 머신 디버깅

enum State { IDLE, RUNNING, PAUSED, ERROR };
State currentState = IDLE;
State lastState = IDLE;

const char* stateNames[] = {"IDLE", "RUNNING", "PAUSED", "ERROR"};

void printState() {
    if (currentState != lastState) {
        Serial.print("[STATE] ");
        Serial.print(stateNames[lastState]);
        Serial.print(" -> ");
        Serial.println(stateNames[currentState]);
        lastState = currentState;
    }
}

void setup() {
    Serial.begin(9600);
    pinMode(2, INPUT_PULLUP);  // Start
    pinMode(3, INPUT_PULLUP);  // Pause
    pinMode(13, OUTPUT);

    Serial.println("State Machine Debug");
    Serial.println("BTN1: Start/Resume, BTN2: Pause");
}

void loop() {
    bool btn1 = !digitalRead(2);
    bool btn2 = !digitalRead(3);

    switch (currentState) {
        case IDLE:
            if (btn1) currentState = RUNNING;
            break;

        case RUNNING:
            digitalWrite(13, (millis() / 500) % 2);  // LED 깜빡임
            if (btn2) currentState = PAUSED;
            break;

        case PAUSED:
            digitalWrite(13, LOW);
            if (btn1) currentState = RUNNING;
            break;

        case ERROR:
            // 에러 처리
            break;
    }

    printState();
    delay(50);
}

8. 실습 프로젝트: 터미널 인터페이스

// terminal_interface.ino
// 완성된 터미널 인터페이스

#define VERSION "1.0.0"
#define MAX_CMD_LEN 64
#define MAX_ARGS 8

// 핀 설정
const int LED_PINS[] = {9, 10, 11, 12, 13};
const int NUM_LEDS = 5;
const int BUTTON_PIN = 2;

// 변수
char cmdBuffer[MAX_CMD_LEN];
int cmdIndex = 0;
bool echoEnabled = true;
unsigned long startTime;

void setup() {
    Serial.begin(9600);

    for (int i = 0; i < NUM_LEDS; i++) {
        pinMode(LED_PINS[i], OUTPUT);
    }
    pinMode(BUTTON_PIN, INPUT_PULLUP);

    startTime = millis();
    printWelcome();
    printPrompt();
}

void printWelcome() {
    Serial.println();
    Serial.println("========================================");
    Serial.println("     Arduino Terminal Interface");
    Serial.print("           Version ");
    Serial.println(VERSION);
    Serial.println("========================================");
    Serial.println("Type 'help' for available commands");
    Serial.println();
}

void printPrompt() {
    Serial.print("> ");
}

void printHelp() {
    Serial.println("\nAvailable commands:");
    Serial.println("  help              - Show this help");
    Serial.println("  led <n> <on/off>  - Control LED 0-4");
    Serial.println("  led all <on/off>  - Control all LEDs");
    Serial.println("  pattern <0-31>    - Set LED pattern");
    Serial.println("  status            - Show system status");
    Serial.println("  button            - Read button state");
    Serial.println("  uptime            - Show uptime");
    Serial.println("  echo <on/off>     - Toggle echo");
    Serial.println("  clear             - Clear screen");
    Serial.println("  reboot            - Soft reboot");
    Serial.println();
}

void cmdLed(int argc, char* argv[]) {
    if (argc < 3) {
        Serial.println("Usage: led <n|all> <on|off>");
        return;
    }

    bool state = (strcmp(argv[2], "on") == 0 || strcmp(argv[2], "1") == 0);

    if (strcmp(argv[1], "all") == 0) {
        for (int i = 0; i < NUM_LEDS; i++) {
            digitalWrite(LED_PINS[i], state);
        }
        Serial.print("All LEDs ");
        Serial.println(state ? "ON" : "OFF");
    } else {
        int led = atoi(argv[1]);
        if (led >= 0 && led < NUM_LEDS) {
            digitalWrite(LED_PINS[led], state);
            Serial.print("LED ");
            Serial.print(led);
            Serial.print(" ");
            Serial.println(state ? "ON" : "OFF");
        } else {
            Serial.println("Invalid LED number (0-4)");
        }
    }
}

void cmdPattern(int argc, char* argv[]) {
    if (argc < 2) {
        Serial.println("Usage: pattern <0-31>");
        return;
    }

    int pattern = atoi(argv[1]) & 0x1F;
    for (int i = 0; i < NUM_LEDS; i++) {
        digitalWrite(LED_PINS[i], (pattern >> i) & 1);
    }

    Serial.print("Pattern: 0b");
    for (int i = 4; i >= 0; i--) {
        Serial.print((pattern >> i) & 1);
    }
    Serial.print(" (");
    Serial.print(pattern);
    Serial.println(")");
}

void cmdStatus() {
    Serial.println("\n--- System Status ---");

    Serial.print("LEDs: ");
    for (int i = 0; i < NUM_LEDS; i++) {
        Serial.print(digitalRead(LED_PINS[i]) ? "1" : "0");
    }
    Serial.println();

    Serial.print("Button: ");
    Serial.println(digitalRead(BUTTON_PIN) ? "Released" : "Pressed");

    Serial.print("Uptime: ");
    printUptime();

    Serial.print("Free RAM: ");
    Serial.print(freeMemory());
    Serial.println(" bytes");

    Serial.println("---------------------\n");
}

void printUptime() {
    unsigned long secs = (millis() - startTime) / 1000;
    int hours = secs / 3600;
    int mins = (secs % 3600) / 60;
    int sec = secs % 60;

    char buf[20];
    sprintf(buf, "%02d:%02d:%02d", hours, mins, sec);
    Serial.println(buf);
}

int freeMemory() {
    extern int __heap_start, *__brkval;
    int v;
    return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}

void processCommand(char* cmd) {
    // 빈 명령 무시
    if (strlen(cmd) == 0) return;

    // 토큰 분리
    char* argv[MAX_ARGS];
    int argc = 0;

    char* token = strtok(cmd, " ");
    while (token && argc < MAX_ARGS) {
        argv[argc++] = token;
        token = strtok(NULL, " ");
    }

    // 소문자로 변환 (명령어만)
    for (int i = 0; argv[0][i]; i++) {
        argv[0][i] = tolower(argv[0][i]);
    }

    // 명령어 처리
    if (strcmp(argv[0], "help") == 0) {
        printHelp();
    }
    else if (strcmp(argv[0], "led") == 0) {
        cmdLed(argc, argv);
    }
    else if (strcmp(argv[0], "pattern") == 0) {
        cmdPattern(argc, argv);
    }
    else if (strcmp(argv[0], "status") == 0) {
        cmdStatus();
    }
    else if (strcmp(argv[0], "button") == 0) {
        Serial.print("Button: ");
        Serial.println(digitalRead(BUTTON_PIN) ? "Released" : "Pressed");
    }
    else if (strcmp(argv[0], "uptime") == 0) {
        Serial.print("Uptime: ");
        printUptime();
    }
    else if (strcmp(argv[0], "echo") == 0) {
        if (argc > 1) {
            echoEnabled = (strcmp(argv[1], "on") == 0);
        }
        Serial.print("Echo: ");
        Serial.println(echoEnabled ? "ON" : "OFF");
    }
    else if (strcmp(argv[0], "clear") == 0) {
        Serial.print("\033[2J\033[H");  // ANSI clear
    }
    else if (strcmp(argv[0], "reboot") == 0) {
        Serial.println("Rebooting...");
        delay(100);
        asm volatile ("jmp 0");  // 소프트 리셋
    }
    else {
        Serial.print("Unknown command: ");
        Serial.println(argv[0]);
        Serial.println("Type 'help' for available commands");
    }
}

void loop() {
    while (Serial.available()) {
        char c = Serial.read();

        // Enter 키
        if (c == '\r' || c == '\n') {
            if (echoEnabled) Serial.println();

            cmdBuffer[cmdIndex] = '\0';
            processCommand(cmdBuffer);
            cmdIndex = 0;

            printPrompt();
        }
        // Backspace
        else if (c == '\b' || c == 127) {
            if (cmdIndex > 0) {
                cmdIndex--;
                if (echoEnabled) {
                    Serial.print("\b \b");  // 지우기
                }
            }
        }
        // 일반 문자
        else if (cmdIndex < MAX_CMD_LEN - 1 && c >= 32) {
            cmdBuffer[cmdIndex++] = c;
            if (echoEnabled) Serial.print(c);
        }
    }
}

연습 문제

연습 1: 온도 로거

시리얼로 "LOG START"를 입력하면 1초마다 가상의 온도 데이터를 출력하고, "LOG STOP"으로 중지하세요.

연습 2: LED 시퀀서

시리얼로 LED 패턴 시퀀스를 입력받아 순차 실행하세요. 예: "SEQ 1,3,5,15,0" → 각 패턴 500ms씩 실행

연습 3: 계산기 확장

괄호와 여러 연산자를 지원하는 계산기로 확장하세요.

연습 4: 이진 통신

PC에서 바이트 시퀀스를 보내면 해석하여 LED를 제어하는 바이너리 프로토콜을 구현하세요.


핵심 개념 정리

함수 설명
Serial.begin() 시리얼 통신 초기화
Serial.print() 데이터 출력
Serial.available() 수신 대기 바이트 수
Serial.read() 1바이트 읽기
Serial.readStringUntil() 구분자까지 읽기
개념 설명
Baud Rate 초당 전송 비트 수
UART 비동기 시리얼 통신
TX/RX 송신/수신 핀
버퍼 수신 데이터 임시 저장소
프로토콜 통신 규약

임베디드 C 기초 완료!

4개의 임베디드 기초 문서를 완료했습니다:

  1. ✅ 임베디드 기초 - 개념, Arduino 환경
  2. ✅ 비트 연산 심화 - 마스킹, 레지스터, volatile
  3. ✅ GPIO 제어 - LED, 버튼, 디바운싱
  4. ✅ 시리얼 통신 - UART, 파싱, 디버깅

다음 학습 추천

기초를 마쳤다면 다음 주제로 확장할 수 있습니다:

  • 타이머와 PWM: LED 밝기 조절, 서보 모터
  • 인터럽트: 외부/타이머 인터럽트, ISR
  • ADC와 센서: 아날로그 입력, 온도/조도 센서
  • I2C/SPI 통신: 센서/디스플레이 연결
  • RTOS: FreeRTOS 기초

실습 플랫폼

  • Wokwi: https://wokwi.com (무료 시뮬레이터)
  • TinkerCAD: https://tinkercad.com/circuits
  • Arduino 공식: https://www.arduino.cc
to navigate between lessons