Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Честный разговор об Arduino

Arduino получил репутацию "игрушки для начинающих". Это несправедливо. Arduino — это доступная платформа с огромной экосистемой, которая используется в реальных устройствах, промышленных прототипах и мелкосерийном производстве.

Да, у неё есть ограничения: 8-битный AVR, 16 МГц, 2 КБ RAM. Но для огромного класса задач этого более чем достаточно. Умный контроллер теплицы, анализатор вибраций, узел сбора данных, промышленный таймер — Arduino справится.

Проблема не в платформе. Проблема в том, как большинство людей пишут код для Arduino. Сегодня покажу как писать Arduino-код правильно — как настоящие инженеры.


Главная ошибка: delay()

delay() — это яд для Arduino. Пока выполняется delay(1000), микроконтроллер ничего не делает. Не обрабатывает кнопки. Не читает датчики. Не обновляет выходы. Он просто спит.

В реальном устройстве с несколькими задачами это неприемлемо.

Правильный подход: millis() и конечный автомат

// ПЛОХО — как делают 90% новичков:
void loop() {
    digitalWrite(LED1, HIGH);
    delay(500);
    digitalWrite(LED1, LOW);
    delay(500);
    
    // Пока мигает LED1 — ничего другого не происходит!
}

// ХОРОШО — неблокирующий код:
class BlinkTask {
private:
    uint8_t pin;
    uint32_t interval;
    uint32_t lastToggle;
    bool state;
    
public:
    BlinkTask(uint8_t _pin, uint32_t _interval)
        : pin(_pin), interval(_interval), lastToggle(0), state(false) {}
    
    void begin() {
        pinMode(pin, OUTPUT);
    }
    
    void update() {
        uint32_t now = millis();
        if (now - lastToggle >= interval) {
            lastToggle = now;
            state = !state;
            digitalWrite(pin, state);
        }
    }
};

BlinkTask led1(13, 500);  // LED на пине 13, период 500 мс
BlinkTask led2(12, 200);  // LED на пине 12, период 200 мс

void setup() {
    led1.begin();
    led2.begin();
}

void loop() {
    led1.update();   // Каждый вызов занимает микросекунды
    led2.update();   // Оба LED мигают независимо!
    // Здесь можно добавить ещё десятки задач
}

Антидребезг кнопок: правильная реализация

Физическая кнопка при нажатии/отпускании "дребезжит" — за несколько миллисекунд создаёт 10–50 ложных переключений. Простой delay(50) тут не поможет — он заблокирует программу.

class Button {
private:
    uint8_t pin;
    uint8_t currentState;
    uint8_t lastRawState;
    uint32_t lastChangeTime;
    uint32_t debounceTime;
    
    // Callbacks
    void (*onPress)();
    void (*onRelease)();
    void (*onLongPress)();
    
    uint32_t pressTime;
    uint32_t longPressTime;
    bool longPressTriggered;
    
public:
    Button(uint8_t _pin, uint32_t _debounce = 50, uint32_t _longPress = 1000)
        : pin(_pin), currentState(HIGH), lastRawState(HIGH),
          lastChangeTime(0), debounceTime(_debounce),
          onPress(nullptr), onRelease(nullptr), onLongPress(nullptr),
          pressTime(0), longPressTime(_longPress), longPressTriggered(false) {}
    
    void begin() {
        pinMode(pin, INPUT_PULLUP);  // Внутренняя подтяжка, нажатие → LOW
    }
    
    void setOnPress(void (*cb)())    { onPress = cb; }
    void setOnRelease(void (*cb)())  { onRelease = cb; }
    void setOnLongPress(void (*cb)()){ onLongPress = cb; }
    
    bool isPressed() { return currentState == LOW; }
    
    void update() {
        uint32_t now = millis();
        uint8_t rawState = digitalRead(pin);
        
        // Антидребезг: состояние должно держаться debounceTime мс
        if (rawState != lastRawState) {
            lastChangeTime = now;
            lastRawState = rawState;
        }
        
        if (now - lastChangeTime >= debounceTime) {
            if (rawState != currentState) {
                currentState = rawState;
                
                if (currentState == LOW) {  // Нажата
                    pressTime = now;
                    longPressTriggered = false;
                    if (onPress) onPress();
                } else {  // Отпущена
                    if (onRelease) onRelease();
                }
            }
        }
        
        // Проверка длинного нажатия
        if (currentState == LOW && !longPressTriggered) {
            if (now - pressTime >= longPressTime) {
                longPressTriggered = true;
                if (onLongPress) onLongPress();
            }
        }
    }
};

// Использование:
Button btn1(2);
Button btn2(3);

int counter = 0;

void onBtn1Press()     { Serial.println("Кнопка 1 нажата"); counter++; }
void onBtn1LongPress() { Serial.println("Длинное нажатие! Сброс"); counter = 0; }
void onBtn2Press()     { Serial.println("Кнопка 2 нажата"); counter--; }

void setup() {
    Serial.begin(115200);
    btn1.begin();
    btn2.begin();
    btn1.setOnPress(onBtn1Press);
    btn1.setOnLongPress(onBtn1LongPress);
    btn2.setOnPress(onBtn2Press);
}

void loop() {
    btn1.update();
    btn2.update();
    // ... другие задачи
}

Конечный автомат (State Machine): управление процессом

Конечный автомат — правильный способ описывать сложное поведение без вложенных if-else и флагов.

Пример: автоматическая стиральная машина (упрощённо):

enum WasherState {
    STATE_IDLE,
    STATE_FILL_WATER,
    STATE_WASH,
    STATE_DRAIN,
    STATE_SPIN,
    STATE_COMPLETE,
    STATE_ERROR
};

enum WasherEvent {
    EVENT_START,
    EVENT_WATER_FULL,
    EVENT_WASH_DONE,
    EVENT_DRAIN_DONE,
    EVENT_SPIN_DONE,
    EVENT_ERROR,
    EVENT_RESET
};

class WashingMachine {
private:
    WasherState state;
    uint32_t stateEnterTime;
    uint32_t washDuration;
    uint32_t spinDuration;
    
    // Пины
    static const uint8_t PIN_VALVE     = 4;   // Клапан залива воды
    static const uint8_t PIN_PUMP      = 5;   // Насос откачки
    static const uint8_t PIN_MOTOR     = 6;   // Двигатель барабана
    static const uint8_t PIN_LED_RUN   = 7;   // Индикатор работы
    static const uint8_t PIN_SENSOR_FULL = 8; // Датчик полного бака
    
    void enterState(WasherState newState) {
        state = newState;
        stateEnterTime = millis();
        
        // Действия при входе в состояние
        switch (state) {
            case STATE_IDLE:
                allOff();
                Serial.println("Ожидание...");
                break;
                
            case STATE_FILL_WATER:
                digitalWrite(PIN_VALVE, HIGH);
                Serial.println("Заполнение водой...");
                break;
                
            case STATE_WASH:
                digitalWrite(PIN_VALVE, LOW);
                digitalWrite(PIN_MOTOR, HIGH);
                Serial.println("Стирка...");
                break;
                
            case STATE_DRAIN:
                digitalWrite(PIN_MOTOR, LOW);
                digitalWrite(PIN_PUMP, HIGH);
                Serial.println("Слив воды...");
                break;
                
            case STATE_SPIN:
                digitalWrite(PIN_PUMP, LOW);
                // Быстрое вращение для отжима
                analogWrite(PIN_MOTOR, 200);  // 78% мощности
                Serial.println("Отжим...");
                break;
                
            case STATE_COMPLETE:
                allOff();
                Serial.println("Стирка завершена!");
                break;
                
            case STATE_ERROR:
                allOff();
                Serial.println("АВАРИЯ!");
                break;
        }
    }
    
    void allOff() {
        digitalWrite(PIN_VALVE, LOW);
        digitalWrite(PIN_PUMP, LOW);
        digitalWrite(PIN_MOTOR, LOW);
    }
    
public:
    WashingMachine(uint32_t _washMin = 30, uint32_t _spinMin = 5)
        : state(STATE_IDLE),
          washDuration(_washMin * 60000UL),
          spinDuration(_spinMin * 60000UL) {}
    
    void begin() {
        pinMode(PIN_VALVE, OUTPUT);
        pinMode(PIN_PUMP, OUTPUT);
        pinMode(PIN_MOTOR, OUTPUT);
        pinMode(PIN_LED_RUN, OUTPUT);
        pinMode(PIN_SENSOR_FULL, INPUT_PULLUP);
        allOff();
    }
    
    void sendEvent(WasherEvent event) {
        switch (state) {
            case STATE_IDLE:
                if (event == EVENT_START)  enterState(STATE_FILL_WATER);
                break;
                
            case STATE_FILL_WATER:
                if (event == EVENT_WATER_FULL) enterState(STATE_WASH);
                if (event == EVENT_ERROR)      enterState(STATE_ERROR);
                break;
                
            case STATE_WASH:
                if (event == EVENT_WASH_DONE)  enterState(STATE_DRAIN);
                if (event == EVENT_ERROR)      enterState(STATE_ERROR);
                break;
                
            case STATE_DRAIN:
                if (event == EVENT_DRAIN_DONE) enterState(STATE_SPIN);
                if (event == EVENT_ERROR)      enterState(STATE_ERROR);
                break;
                
            case STATE_SPIN:
                if (event == EVENT_SPIN_DONE)  enterState(STATE_COMPLETE);
                break;
                
            case STATE_ERROR:
                if (event == EVENT_RESET)      enterState(STATE_IDLE);
                break;
                
            default:
                break;
        }
    }
    
    // Вызывать в loop() каждый цикл
    void update() {
        uint32_t elapsed = millis() - stateEnterTime;
        
        // Индикатор работы
        bool running = (state != STATE_IDLE && state != STATE_COMPLETE 
                                             && state != STATE_ERROR);
        digitalWrite(PIN_LED_RUN, running ? (millis() % 500 < 250) : LOW);
        
        switch (state) {
            case STATE_FILL_WATER:
                // Проверяем датчик уровня
                if (digitalRead(PIN_SENSOR_FULL) == LOW) {
                    sendEvent(EVENT_WATER_FULL);
                }
                // Таймаут заполнения 10 минут
                if (elapsed > 600000UL) {
                    sendEvent(EVENT_ERROR);
                }
                break;
                
            case STATE_WASH:
                if (elapsed >= washDuration) {
                    sendEvent(EVENT_WASH_DONE);
                }
                break;
                
            case STATE_DRAIN:
                // 3 минуты на слив
                if (elapsed >= 180000UL) {
                    sendEvent(EVENT_DRAIN_DONE);
                }
                break;
                
            case STATE_SPIN:
                if (elapsed >= spinDuration) {
                    sendEvent(EVENT_SPIN_DONE);
                }
                break;
                
            default:
                break;
        }
    }
    
    WasherState getState() { return state; }
};

WashingMachine washer(30, 5);  // 30 мин стирка, 5 мин отжим
Button startBtn(2);

void setup() {
    Serial.begin(115200);
    washer.begin();
    startBtn.begin();
    startBtn.setOnPress([]() {
        washer.sendEvent(EVENT_START);
    });
}

void loop() {
    washer.update();
    startBtn.update();
}

ПИД-регулятор: управление температурой

class PIDController {
private:
    float kp, ki, kd;
    float setpoint;
    float integral;
    float prevError;
    float integralLimit;
    float outputMin, outputMax;
    uint32_t lastTime;
    
public:
    PIDController(float _kp, float _ki, float _kd,
                  float _outMin = 0.0f, float _outMax = 100.0f)
        : kp(_kp), ki(_ki), kd(_kd),
          setpoint(0), integral(0), prevError(0),
          integralLimit(50.0f),
          outputMin(_outMin), outputMax(_outMax),
          lastTime(0) {}
    
    void setSetpoint(float sp) { setpoint = sp; }
    void reset() { integral = 0; prevError = 0; }
    
    float compute(float measured) {
        uint32_t now = millis();
        float dt = (now - lastTime) / 1000.0f;  // Секунды
        lastTime = now;
        
        if (dt <= 0 || dt > 1.0f) dt = 0.1f;  // Защита от аномальных dt
        
        float error = setpoint - measured;
        
        // Интегральная составляющая с ограничением (anti-windup)
        integral += error * dt;
        integral = constrain(integral, -integralLimit, integralLimit);
        
        // Производная (фильтруем шум — берём производную измерения, не ошибки)
        float derivative = -(measured - prevError) / dt;  // -d(PV)/dt
        prevError = measured;
        
        float output = kp * error + ki * integral + kd * derivative;
        return constrain(output, outputMin, outputMax);
    }
};

// Применение: ПИД-термостат с твёрдотельным реле (SSR)
const uint8_t PIN_HEATER   = 9;    // ШИМ-выход на SSR
const uint8_t PIN_TEMP_SCL = A4;   // I2C — датчик температуры
const uint8_t PIN_TEMP_SDA = A5;

PIDController tempPID(2.0f, 0.5f, 1.0f, 0.0f, 255.0f);

float readTemperature() {
    // Здесь чтение датчика DS18B20 или термопары MAX6675
    // Возвращаем демо-значение:
    return 25.0f + random(-10, 10) / 10.0f;
}

void setup() {
    Serial.begin(115200);
    pinMode(PIN_HEATER, OUTPUT);
    tempPID.setSetpoint(75.0f);  // Уставка 75°C
}

void loop() {
    static uint32_t lastPID = 0;
    
    if (millis() - lastPID >= 500) {  // ПИД каждые 500 мс
        lastPID = millis();
        
        float temp = readTemperature();
        float output = tempPID.compute(temp);
        
        analogWrite(PIN_HEATER, (uint8_t)output);
        
        Serial.print("T="); Serial.print(temp, 1);
        Serial.print("°C  SP=75°C  OUT=");
        Serial.print((int)output * 100 / 255);
        Serial.println("%");
    }
}

Modbus RTU Slave на Arduino

#include <SoftwareSerial.h>

// RS-485 через MAX485
SoftwareSerial rs485(10, 11);  // RX, TX
const uint8_t DE_RE_PIN = 4;   // Driver Enable / Receiver Enable

// Данные устройства (10 Holding Registers)
uint16_t holdingRegs[10] = {0};
uint16_t inputRegs[10]   = {0};

const uint8_t DEVICE_ADDRESS = 1;

uint16_t crc16(uint8_t *buf, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= buf[i];
        for (uint8_t j = 0; j < 8; j++) {
            crc = (crc & 1) ? ((crc >> 1) ^ 0xA001) : (crc >> 1);
        }
    }
    return crc;
}

void rs485Send(uint8_t *data, uint8_t len) {
    digitalWrite(DE_RE_PIN, HIGH);  // Режим передачи
    delayMicroseconds(100);
    rs485.write(data, len);
    rs485.flush();
    delayMicroseconds(100);
    digitalWrite(DE_RE_PIN, LOW);   // Режим приёма
}

void processModbus(uint8_t *req, uint8_t len) {
    if (req[0] != DEVICE_ADDRESS) return;
    
    uint16_t rxCRC = (req[len-1] << 8) | req[len-2];
    if (crc16(req, len-2) != rxCRC) return;
    
    uint8_t resp[64];
    uint8_t rlen = 0;
    uint8_t fc   = req[1];
    uint16_t addr  = (req[2] << 8) | req[3];
    uint16_t count = (req[4] << 8) | req[5];
    
    resp[rlen++] = DEVICE_ADDRESS;
    resp[rlen++] = fc;
    
    if (fc == 0x03 && addr + count <= 10) {  // Read Holding Registers
        resp[rlen++] = count * 2;
        for (uint16_t i = 0; i < count; i++) {
            resp[rlen++] = holdingRegs[addr + i] >> 8;
            resp[rlen++] = holdingRegs[addr + i] & 0xFF;
        }
    }
    else if (fc == 0x04 && addr + count <= 10) {  // Read Input Registers
        resp[rlen++] = count * 2;
        for (uint16_t i = 0; i < count; i++) {
            resp[rlen++] = inputRegs[addr + i] >> 8;
            resp[rlen++] = inputRegs[addr + i] & 0xFF;
        }
    }
    else if (fc == 0x06 && addr < 10) {  // Write Single Register
        holdingRegs[addr] = (req[4] << 8) | req[5];
        memcpy(resp + 2, req + 2, 4);
        rlen += 4;
    }
    else {
        resp[1] |= 0x80;
        resp[rlen++] = (fc == 0x03 || fc == 0x04 || fc == 0x06) ? 0x02 : 0x01;
    }
    
    uint16_t c = crc16(resp, rlen);
    resp[rlen++] = c & 0xFF;
    resp[rlen++] = c >> 8;
    
    rs485Send(resp, rlen);
}

void setup() {
    Serial.begin(115200);
    rs485.begin(9600);
    pinMode(DE_RE_PIN, OUTPUT);
    digitalWrite(DE_RE_PIN, LOW);  // Режим приёма по умолчанию
}

void loop() {
    // Обновляем Input Registers реальными данными
    inputRegs[0] = analogRead(A0);           // Аналоговый вход 0-1023
    inputRegs[1] = (uint16_t)(millis() / 1000); // Uptime в секундах
    
    // Принимаем Modbus-запросы
    static uint8_t rxBuf[64];
    static uint8_t rxLen = 0;
    static uint32_t lastByte = 0;
    
    while (rs485.available()) {
        if (rxLen < sizeof(rxBuf)) {
            rxBuf[rxLen++] = rs485.read();
        }
        lastByte = millis();
    }
    
    // Конец фрейма — пауза 3.5 символа (при 9600 бод ~4 мс)
    if (rxLen > 0 && millis() - lastByte > 4) {
        processModbus(rxBuf, rxLen);
        rxLen = 0;
    }
}

Работа с несколькими датчиками I2C

#include <Wire.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_SHT31.h>
#include <RTC_DS3231.h>

Adafruit_BMP280 bmp;
Adafruit_SHT31 sht;
RTC_DS3231 rtc;

struct SensorData {
    float temperature_bmp;
    float pressure_hpa;
    float temperature_sht;
    float humidity;
    uint32_t timestamp;
    bool bmp_ok;
    bool sht_ok;
};

SensorData readAllSensors() {
    SensorData data = {0};
    data.timestamp = rtc.now().unixtime();
    
    // BMP280: давление и температура
    if (bmp.begin(0x76)) {
        data.temperature_bmp = bmp.readTemperature();
        data.pressure_hpa    = bmp.readPressure() / 100.0f;
        data.bmp_ok = true;
    }
    
    // SHT31: температура и влажность
    if (sht.begin(0x44)) {
        data.temperature_sht = sht.readTemperature();
        data.humidity        = sht.readHumidity();
        data.sht_ok = (!isnan(data.temperature_sht));
    }
    
    return data;
}

void printSensorData(const SensorData& d) {
    Serial.print("T1="); Serial.print(d.temperature_bmp, 1); Serial.print("°C ");
    Serial.print("P=");  Serial.print(d.pressure_hpa, 1);    Serial.print("гПа ");
    Serial.print("T2="); Serial.print(d.temperature_sht, 1); Serial.print("°C ");
    Serial.print("H=");  Serial.print(d.humidity, 1);        Serial.println("%");
}

void setup() {
    Serial.begin(115200);
    Wire.begin();
    Wire.setClock(400000);  // Fast mode 400 кГц
    
    if (!bmp.begin(0x76)) Serial.println("BMP280 не найден!");
    if (!sht.begin(0x44)) Serial.println("SHT31 не найден!");
    if (!rtc.begin())     Serial.println("DS3231 не найден!");
}

void loop() {
    static uint32_t lastRead = 0;
    
    if (millis() - lastRead >= 5000) {  // Каждые 5 секунд
        lastRead = millis();
        SensorData data = readAllSensors();
        printSensorData(data);
    }
}

Запись данных на SD-карту

#include <SPI.h>
#include <SD.h>

const uint8_t SD_CS_PIN = 10;

bool sdAvailable = false;

void sdInit() {
    sdAvailable = SD.begin(SD_CS_PIN);
    if (!sdAvailable) {
        Serial.println("SD-карта не найдена!");
    } else {
        Serial.println("SD-карта готова.");
    }
}

void logData(const String& filename, float temp, float humidity, 
             uint32_t timestamp) {
    if (!sdAvailable) return;
    
    File file = SD.open(filename, FILE_WRITE);
    if (!file) {
        Serial.println("Ошибка открытия файла!");
        return;
    }
    
    // CSV-формат
    file.print(timestamp);
    file.print(',');
    file.print(temp, 2);
    file.print(',');
    file.println(humidity, 2);
    
    file.close();
}

// Создание нового файла каждый день
String getDailyFilename(uint32_t unixtime) {
    // Простой расчёт дня
    uint32_t day = unixtime / 86400;
    return "LOG_" + String(day) + ".CSV";
}

Советы по надёжности Arduino-проектов

1. Сторожевой таймер (Watchdog)

#include <avr/wdt.h>

void setup() {
    wdt_enable(WDTO_2S);  // Сброс если loop() не выполняется 2 секунды
}

void loop() {
    wdt_reset();  // Сброс таймера — "я ещё живой"
    // ... ваш код
}

2. EEPROM для сохранения настроек

#include <EEPROM.h>

struct Settings {
    float setpoint;
    uint16_t interval;
    uint8_t mode;
    uint16_t checksum;
};

void saveSettings(const Settings& s) {
    Settings toSave = s;
    // Простая контрольная сумма
    toSave.checksum = (uint16_t)(s.setpoint * 100) + s.interval + s.mode;
    EEPROM.put(0, toSave);
}

bool loadSettings(Settings& s) {
    EEPROM.get(0, s);
    uint16_t expected = (uint16_t)(s.setpoint * 100) + s.interval + s.mode;
    return (s.checksum == expected);  // Данные корректны?
}

3. Ограничение частоты Serial

// НЕ ПИШИТЕ В Serial КАЖДЫЙ ЦИКЛ LOOP!
// При 115200 бод запись 100 байт занимает ~8мс

void printPeriodic(float value) {
    static uint32_t lastPrint = 0;
    if (millis() - lastPrint >= 200) {  // Максимум 5 раз в секунду
        lastPrint = millis();
        Serial.println(value);
    }
}

Заключение

Arduino — это не учебная игрушка, это платформа. Разница между "поделкой" и "устройством" — не в железе, а в качестве кода.

Ключевые принципы: никогда не используйте delay() в итоговом устройстве, структурируйте код как набор независимых задач через millis(), используйте конечные автоматы для сложной логики, добавляйте watchdog timer и защиты, документируйте код.

С этими принципами Arduino становится полноценным инструментом для создания надёжных устройств — от умного дома до промышленных узлов сбора данных.

User Feedback

Create an account or sign in to leave a review

There are no reviews to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.