Честный разговор об 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 становится полноценным инструментом для создания надёжных устройств — от умного дома до промышленных узлов сбора данных.
Create an account or sign in to leave a review
There are no reviews to display.