✅ کلید پاسخ

کلید پاسخ آزمون پیشرفته Arduino

پاسخ‌نامه تشریحی و کامل سؤالات آزمون
📝 تعداد سؤالات ۱۶
📊 نمره کل ۱۰۰
🎯 سطح پیشرفته

سؤال ۱ — millis و Overflow

الف) چرا "==" مناسب نیست؟

چون millis() در هر لحظه دقیقاً یک مقدار مشخص برمی‌گرداند و ممکن است مقدار مورد انتظار دقیقاً دیده نشود.

مثلاً اگر بنویسیم:

if (millis() == previous + interval)

ممکن است مقدار millis() از مقدار موردنظر عبور کند و شرط هیچ‌وقت اجرا نشود.

روش صحیح

unsigned long previous = 0;
const unsigned long interval = 1000;

void loop() {
    unsigned long current = millis();

    if (current - previous >= interval) {
        Serial.println("EVENT");
        previous = current;
    }
}

ب) Overflow چیست؟

millis() بعد از رسیدن به حداکثر مقدار قابل نمایش نوع داده، سرریز می‌شود و از مقدار بسیار بزرگ دوباره به مقدار نزدیک صفر برمی‌گردد. بنابراین نباید منطق برنامه را بر اساس مقایسه مستقیم زمان‌ها طراحی کنیم.

ج) چرا تفریق بهتر است؟

عبارت current - previous >= interval به شکلی طراحی می‌شود که حتی هنگام سرریز millis() نیز بتواند اختلاف زمانی را به‌درستی بررسی کند.

د) تفاوت دو روش

previous = millis();

در این روش زمان اجرای واقعی ثبت می‌شود و اگر اجرای برنامه کمی تأخیر داشته باشد، فاصله‌ها ممکن است کمی جابه‌جا شوند.

در روش previous += interval;

زمان‌بندی بر اساس دوره‌های مشخص ادامه پیدا می‌کند و برای کارهای دوره‌ای می‌تواند drift کمتری داشته باشد؛ البته باید شرایط عقب‌افتادگی برنامه نیز در نظر گرفته شود.

سؤال ۲ — uint8_t و Underflow

کد:

uint8_t i = 10;

while (i >= 0) {
    Serial.println(i);
    i--;
}

مشکل چیست؟

uint8_t فقط اعداد بدون علامت را نگه می‌دارد و بازه آن 0 تا 255 است.

بنابراین وقتی i = 0; و سپس i--; انجام شود، مقدار به 255 برمی‌گردد.

در نتیجه شرط i >= 0 برای uint8_t همیشه درست است و حلقه می‌تواند بی‌نهایت ادامه پیدا کند.

راه‌حل ۱

استفاده از int:

int i = 10;

while (i >= 0) {
    Serial.println(i);
    i--;
}

راه‌حل ۲

استفاده از شرط مناسب برای unsigned:

uint8_t i = 10;

while (i > 0) {
    Serial.println(i);
    i--;
}

Serial.println(0);

سؤال ۳ — break و continue

کد:

for (int i = 0; i < 10; i++) {

    if (digitalRead(BUTTON) == LOW) {
        continue;
    }

    if (counter >= 5) {
        break;
    }

    counter++;
}

"continue"

باعث می‌شود ادامه دستورات همان iteration اجرا نشود و حلقه به iteration بعدی برود. یعنی در این مثال continue; باعث می‌شود دستورات بعد از آن در همان دور اجرا نشوند.

"break"

کاملاً از حلقه خارج می‌شود. بنابراین if (counter >= 5) { break; } باعث خروج از for می‌شود.

وقتی counter برابر 5 شود

شرط counter >= 5 درست شده و break اجرا می‌شود؛ بنابراین حلقه همان‌جا تمام می‌شود.

کاربردها

"break":

if (condition) {
    break;
}

برای خروج زودهنگام از حلقه.

"continue":

if (invalid) {
    continue;
}

برای رد کردن iteration فعلی.

در پروژه‌های Arduino، continue می‌تواند برای نادیده گرفتن داده نامعتبر و break برای خروج از جست‌وجو یا پردازش یک حلقه استفاده شود.

سؤال ۴ — EEPROM

کد:

void loop() {
    int value = analogRead(A0);
    EEPROM.write(10, value);
    delay(100);
}

مشکلات

  1. analogRead() در Arduino Uno مقداری بین 0 تا 1023 می‌دهد.
  2. EEPROM.write() فقط یک byte می‌نویسد؛ بنابراین نمی‌توان مقدار 1023 را به شکل کامل در یک byte ذخیره کرد.
  3. نوشتن مداوم در EEPROM باعث فرسایش سلول‌های EEPROM می‌شود.
  4. delay(100) اجرای سایر بخش‌های برنامه را متوقف می‌کند.
  5. اگر مقدار تغییر نکرده، بهتر است از نوشتن غیرضروری جلوگیری شود.

اگر فقط یک byte لازم باشد

EEPROM.update(address, value);
update() فقط زمانی می‌نویسد که مقدار جدید با مقدار قبلی متفاوت باشد.

برای ذخیره int

می‌توان از:

EEPROM.put(address, value);

استفاده کرد. و برای خواندن:

EEPROM.get(address, value);

نکته مهم

EEPROM.update() مشکل فرسایش را کاملاً حل نمی‌کند؛ فقط تعداد نوشتن‌های غیرضروری را کاهش می‌دهد.

سؤال ۵ — سیستم هشدار بدون Blocking

شرایط:

نمونه پاسخ

enum AlarmState {
    NORMAL,
    WARNING,
    DANGER
};

AlarmState state = NORMAL;

unsigned long previousMillis = 0;
const unsigned long interval = 100;

#define LED_PIN 9
#define BUZZER_PIN 3

void updateAlarm(int distance) {

    if (distance > 100) {
        state = NORMAL;
    }
    else if (distance >= 50) {
        state = WARNING;
    }
    else {
        state = DANGER;
    }
}

void handleNormal() {
    digitalWrite(LED_PIN, LOW);
    noTone(BUZZER_PIN);
}

void handleWarning() {
    digitalWrite(LED_PIN, HIGH);
    tone(BUZZER_PIN, 500);
}

void handleDanger() {
    noTone(BUZZER_PIN);

    int pwm = map(analogRead(A0), 0, 1023, 0, 255);
    analogWrite(LED_PIN, pwm);

    tone(BUZZER_PIN, 1000);
}

void loop() {

    unsigned long current = millis();

    if (current - previousMillis >= interval) {
        previousMillis = current;

        // خواندن سنسور
        // updateAlarm(distance);
    }

    switch (state) {

        case NORMAL:
            handleNormal();
            break;

        case WARNING:
            handleWarning();
            break;

        case DANGER:
            handleDanger();
            break;
    }

    // در این قسمت HC-05 نیز می‌تواند
    // بدون delay پردازش شود.
}
نکته اصلی: برنامه نباید با delay() متوقف شود و بخش‌های مختلف باید بتوانند به‌صورت غیرمسدودکننده اجرا شوند.

سؤال ۶ — کنترل موتور با L298N

پایه‌ها:

#define ENA 5
#define IN1 7
#define IN2 8

تابع باید سرعت بین -255 تا 255 بگیرد.

void setMotor(int speed) {

    speed = constrain(speed, -255, 255);

    if (speed > 0) {

        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);

        analogWrite(ENA, speed);
    }

    else if (speed < 0) {

        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);

        analogWrite(ENA, -speed);
    }

    else {

        digitalWrite(IN1, LOW);
        digitalWrite(IN2, LOW);

        analogWrite(ENA, 0);
    }
}

نکته مهم

PWM منفی نداریم. یعنی analogWrite(ENA, -100); مفهوم درستی ندارد. عدد منفی برای تعیین جهت موتور استفاده می‌شود.
ENA برای کنترل توان/PWM و IN1 و IN2 برای تعیین جهت استفاده می‌شوند.

سؤال ۷ — map و constrain

هدف:

ADC: 120 تا 900
        ↓
PWM: 40 تا 220

تابع: uint8_t calculatePWM(int sensorValue)

پاسخ مناسب

uint8_t calculatePWM(int sensorValue) {

    sensorValue = constrain(sensorValue, 120, 900);

    int pwm = map(
        sensorValue,
        120, 900,
        40, 220
    );

    pwm = constrain(pwm, 40, 220);

    return (uint8_t)pwm;
}

نکته مهم

map() به‌تنهایی مقدار ورودی را محدود نمی‌کند. مثلاً اگر sensorValue = 1000; باشد، map() می‌تواند مقداری خارج از محدوده موردنظر تولید کند. پس برای اطمینان از constrain استفاده می‌کنیم.

سؤال ۸ — ارسال پارامتر با Reference

تابع باید سه عدد بگیرد و موارد زیر را برگرداند:

بدون استفاده از متغیر global.

void analyzeValues(
    int a,
    int b,
    int c,
    int &minimum,
    int &maximum,
    float &average,
    int &range
) {

    minimum = a;

    if (b < minimum)
        minimum = b;

    if (c < minimum)
        minimum = c;


    maximum = a;

    if (b > maximum)
        maximum = b;

    if (c > maximum)
        maximum = c;


    average = (a + b + c) / 3.0;

    range = maximum - minimum;
}

مثلاً:

int minValue;
int maxValue;
float avg;
int range;

analyzeValues(
    10,
    25,
    17,
    minValue,
    maxValue,
    avg,
    range
);

نتیجه:

minimum = 10
maximum = 25
average = 17.33
range = 15
استفاده از 3.0 مهم است تا تقسیم به صورت اعشاری انجام شود.

سؤال ۹ — Parser برای HC-05

دستورات ورودی:

AUTO
MANUAL
MOTOR:150
MOTOR:-100
LED:80
BUZZER:1000

یک ساختار مناسب:

enum Mode {
    AUTO_MODE,
    MANUAL_MODE
};

Mode mode = AUTO_MODE;

char buffer[32];
byte index = 0;

دریافت کاراکترها

void receiveBluetooth() {

    while (Serial.available()) {

        char c = Serial.read();

        // نادیده گرفتن کاراکترهای نامعتبر
        if (c == '\r') {
            continue;
        }

        // پایان دستور
        if (c == '\n') {
            buffer[index] = '\0';
            processCommand(buffer);
            index = 0;
            break;
        }

        if (index < sizeof(buffer) - 1) {
            buffer[index++] = c;
        }
    }
}

پردازش

void processCommand(char *cmd) {

    if (strcmp(cmd, "AUTO") == 0) {
        mode = AUTO_MODE;
        return;
    }

    if (strcmp(cmd, "MANUAL") == 0) {
        mode = MANUAL_MODE;
        return;
    }

    if (strncmp(cmd, "MOTOR:", 6) == 0) {

        int value = atoi(cmd + 6);

        value = constrain(value, -255, 255);

        setMotor(value);

        return;
    }

    if (strncmp(cmd, "LED:", 4) == 0) {

        int value = atoi(cmd + 4);

        value = constrain(value, 0, 255);

        analogWrite(LED_PIN, value);

        return;
    }

    if (strncmp(cmd, "BUZZER:", 7) == 0) {

        int frequency = atoi(cmd + 7);

        if (frequency > 0) {
            tone(BUZZER_PIN, frequency);
        }
        else {
            noTone(BUZZER_PIN);
        }

        return;
    }
}

ساختار منطقی Parser

Receive
   ↓
Buffer
   ↓
تشخیص کامل شدن دستور
   ↓
Parse
   ↓
Validate
   ↓
Execute
continue می‌تواند برای رد کردن کاراکترهای غیرضروری استفاده شود و break برای خروج از حلقه دریافت پس از کامل شدن دستور.

سؤال ۱۰ — State Machine

چهار حالت:

IDLE
WARNING
DANGER
ERROR

شرایط:

distance > 100      → IDLE
50 <= distance <=100 → WARNING
distance < 50       → DANGER
distance < 0        → ERROR

تعریف

enum SystemState {
    IDLE,
    WARNING,
    DANGER,
    ERROR_STATE
};

SystemState currentState = IDLE;

تابع تعیین State

SystemState calculateState(int distance) {

    if (distance < 0)
        return ERROR_STATE;

    if (distance < 50)
        return DANGER;

    if (distance <= 100)
        return WARNING;

    return IDLE;
}

تشخیص تغییر State

void updateState(int distance) {

    SystemState newState = calculateState(distance);

    if (newState != currentState) {

        currentState = newState;

        executeStateChange(currentState);
    }
}

اجرای رویداد هنگام تغییر

void executeStateChange(SystemState state) {

    switch (state) {

        case IDLE:
            noTone(BUZZER_PIN);
            // LCD: IDLE
            break;

        case WARNING:
            tone(BUZZER_PIN, 500);
            // LCD: WARNING
            break;

        case DANGER:
            tone(BUZZER_PIN, 1000);
            // LCD: DANGER
            break;

        case ERROR_STATE:
            noTone(BUZZER_PIN);
            // LCD: ERROR
            break;
    }
}

نکته مهم

در State Machine بهتر است دستورات مربوط به ورود به یک State فقط زمانی اجرا شوند که State واقعاً تغییر کرده باشد: if (newState != currentState) و از delay() استفاده نشود تا سایر قسمت‌های برنامه بتوانند همزمان اجرا شوند.

📌 نکات کلیدی آزمون