Полезные алгоритмы Arduino. Обновляемая статья!

На этой странице буду публиковать некоторые полезные алгоритмы и примеры для ваших проектов, которые накопились у меня за годы разработки собственных. Статья обновляется по мере моей ленивости, так что иногда заходите, проверяйте =)

  • Автоформатирование – Arduino IDE умеет автоматически приводить ваш код в порядок (имеются в виду отступы, переносы строк и пробелы). Для автоматического форматирования используйте комбинацию CTRL+T на клавиатуре, либо Инструменты/АвтоФорматирование в окне IDE. Используйте чаще, чтобы сделать код красивым (каноничным, классическим) и более читаемым для других!
  • Скрытие частей кода – сворачивайте длинные функции и прочие куски кода для экономии места и времени на скроллинг. Включается здесь: Файл/Настройки/Включить сворачивание кода
  • Не используйте мышку! Чем выше становится ваш навык в программировании, тем меньше вы будете использовать мышку (да-да, как в фильмах про хакеров). Используйте обе руки для написания кода и перемещения по нему, вот вам несколько полезных комбинаций и хаков, которыми я пользуюсь ПОСТОЯННО:

    • Ctrl+← , Ctrl+→ – переместить курсор влево/вправо НА ОДНО СЛОВО
    • Home , End – переместить курсор в начало/конец строки
    • Shift+← , Shift+→ – выделить символ слева/справа от курсора
    • Shift+Ctrl+← , Shift+Ctrl+→ – выделить слово слева/справа от курсора
    • Shift+Home , Shift+End – выделить все символы от текущего положения курсора до начала/конца строки
    • Ctrl+Z – отменить последнее действие
    • Ctrl+Y – повторить отменённое действие
    • Ctrl+C – копировать выделенный текст
    • Ctrl+X – вырезать выделенный текст
    • Ctrl+V – вставить текст из буфера обмена

    Местные сочетания:

    • Ctrl+U – загрузить прошивку в Arduino
    • Ctrl+R – скомпилировать (проверить)
    • Ctrl+Shift+M – открыть монитор порта

    Также для отодвигания комментариев в правую часть кода используйте TAB, а не ПРОБЕЛ. Нажатие TAB перемещает курсор по некоторой таблице, из-за чего ваши комментарии будут установлены красиво на одном расстоянии за вдвое меньшее количество нажатий!

Питание от пинов – во время разработки прототипов без брэдборда всегда не хватает пинов для питания датчиков и модулей. Так вот, слабые (с потреблением тока менее 40 мА) 5 Вольтовые датчики можно питать от любых пинов! Достаточно сформировать пин как выход, и подать на него нужный сигнал (HIGH – 5 Вольт, LOW – GND).

Пример: подключаем трёхпиновый датчик звука, не используя пины 5V и GND

#define SENSOR_VCC 2    // пин VCC сенсора на D2
#define SENSOR_GND 3    // пин GND сенсора на D3
#define SENSOR_OUT 4    // сигнальный пин на D4
void setup() {
  // настройка пинов
  pinMode(SENSOR_VCC, OUTPUT);
  pinMode(SENSOR_GND, OUTPUT);
  // подаём сигналы
  digitalWrite(SENSOR_VCC, HIGH);
  digitalWrite(SENSOR_GND, LOW);
}
void loop() {
  // в качестве примера считываем сигнал
  boolean sound_signal = digitalRead(SENSOR_OUT);
}

Питание от штекера для программатора. Вы наверняка задавались вопросом, а зачем на Arduino NANO на краю платы расположены 6 пинов? Это порт для подключения ISP программатора. Что он делает в списке лайфхаков? Вот вам фото распиновки, используйте!

  • Использовать библиотеку энергосбережения GyverPower, есть подробный урок
  • В паре с библиотекой сделать несколько модификаций: отключить светодиод питания и отрезать левую ногу регулятора напряжения. ВНИМАНИЕ! Резать ногу регулятору можно только в том случае, если плата питается от источника 3-5 Вольт в пины 5V и GND.

Arduino Pro Mini бывает двух типов: с кварцем на 16 МГц и 8 МГц. Китайцы обычно не подписывают плату, и есть риск перепутать разные платы, если у вас есть и те и те. На средних по цене Pro Mini стоит качественный полноразмерный кварц в овальном металлическом корпусе, на нём крупно написана цифра, обозначающая частоту в Мгц:

На недорогих платах стоит крошечный дешёвый кварц в SMD корпусе, вот он:

Берём лупу и смотрим: 16 МГц кварц маркируется примерно как “A1” or “A’N”, 8 МГц кварц маркируется “80’0” или что-то в этом стиле. Ну вот, теперь вы не перепутаете свои Pro Mini!

РАБОТА С ПЕРИФЕРИЕЙ (ДЛЯ ATMEGA328)


Или облегчённые и ускоренные куски ядра Arduino и не только

void pinModeFast(uint8_t pin, uint8_t mode) {
  switch (mode) {
    case INPUT:
      if (pin < 8) {
        bitClear(DDRD, pin);		
        bitClear(PORTD, pin);
      } else if (pin < 14) {
        bitClear(DDRB, (pin - 8));
        bitClear(PORTB, (pin - 8));
      } else if (pin < 20) {
        bitClear(DDRC, (pin - 14));	 // Mode: INPUT
        bitClear(PORTC, (pin - 8));  // State: LOW
      }
      return;
    case OUTPUT:
      if (pin < 8) {
        bitSet(DDRD, pin);
        bitClear(PORTD, pin);
      } else if (pin < 14) {
        bitSet(DDRB, (pin - 8));
        bitClear(PORTB, (pin - 8));
      } else if (pin < 20) {
        bitSet(DDRC, (pin - 14));	// Mode: OUTPUT
        bitClear(PORTC, (pin - 8));	// State: LOW
      }
      return;
    case INPUT_PULLUP:
      if (pin < 8) {
        bitClear(DDRD, pin);
        bitSet(PORTD, pin);
      } else if (pin < 14) {
        bitClear(DDRB, (pin - 8));
        bitSet(PORTB, (pin - 8));
      } else if (pin < 20) {
        bitClear(DDRC, (pin - 14));	// Mode: OUTPUT
        bitSet(PORTC, (pin - 14));	// State: HIGH
      }
      return;
  }
}
void digitalWriteFast(uint8_t pin, bool x) {
  switch (pin) {						
    case 3: bitClear(TCCR2A, COM2B1);
      break;
    case 5: bitClear(TCCR0A, COM0B1);
      break;
    case 6: bitClear(TCCR0A, COM0A1);
      break;
    case 9: bitClear(TCCR1A, COM1A1);
      break;
    case 10: bitClear(TCCR1A, COM1B1);
      break;
    case 11: bitClear(TCCR2A, COM2A1); 	// PWM disable 
      break;
  }

  if (pin < 8) {
    bitWrite(PORTD, pin, x);
  } else if (pin < 14) {
    bitWrite(PORTB, (pin - 8), x);
  } else if (pin < 20) {
    bitWrite(PORTC, (pin - 14), x);		// Set pin to HIGH / LOW 
  }
}
// быстро инвертирует состояние пина
void digitalToggleFast(uint8_t pin) {
  if (pin < 8) {
    bitSet(PIND, pin);
  } else if (pin < 14) {
    bitSet(PINB, (pin - 8));
  } else if (pin < 20) {
    bitSet(PINC, (pin - 14));		// Toggle pin state (for 'tone()')
  }
}
bool digitalReadFast(uint8_t pin) {
  if (pin < 8) {
    return bitRead(PIND, pin);
  } else if (pin < 14) {
    return bitRead(PINB, pin - 8);
  } else if (pin < 20) {
    return bitRead(PINC, pin - 14);		// Return pin state
  }
}
void analogWriteFast(uint8_t pin, uint16_t duty) {
  
  if (!duty) {					// If duty = 0
    digitalWrite(pin, LOW);		// Disable PWM and set pin to LOW
    return;						// Skip next code
  }

  switch (pin) {
  case 5:
    bitSet(TCCR0A, COM0B1);		// Enable hardware timer output
    OCR0B = duty;				// Load duty to compare register
    return;
  case 6:
    bitSet(TCCR0A, COM0A1);
    OCR0A = duty;
    return;
  case 10:
    bitSet(TCCR1A, COM1B1);
    OCR1B = duty;
    return;
  case 9:
    bitSet(TCCR1A, COM1A1);
    OCR1A = duty;
    return;
  case 3:
    bitSet(TCCR2A, COM2B1);
    OCR2B = duty;
    return;
  case 11:
    bitSet(TCCR2A, COM2A1);
    OCR2A = duty;
    return;
  }
}
// ВНИМАНИЕ! Нужное опорное установлено DEFAULT, можно изменить на своё
uint16_t analogReadFast(uint8_t pin) {
  pin = ((pin < 8) ? pin : pin - 14);		// analogRead(2) = analogRead(A2)
  ADMUX = (DEFAULT<< 6) | pin;	  // Set analog MUX & reference
  bitSet(ADCSRA, ADSC);						// Start 
  while (ADCSRA & (1 << ADSC));				// Wait
  return ADC;								// Return result
}
// установка делителя АЦП. Доступны 2,4,8,16,32,64,128
  
void analogPrescaler(uint8_t prescaler) {
  switch (prescaler) {
    case 2: ADCSRA = (ADCSRA & 0xF8) | 0x01;
      break;
    case 4: ADCSRA = (ADCSRA & 0xF8) | 0x02;
      break;
    case 8: ADCSRA = (ADCSRA & 0xF8) | 0x03;
      break;
    case 16: ADCSRA = (ADCSRA & 0xF8) | 0x04;
      break;
    case 32: ADCSRA = (ADCSRA & 0xF8) | 0x05;
      break;
    case 64: ADCSRA = (ADCSRA & 0xF8) | 0x06;
      break;
    case 128: ADCSRA = (ADCSRA & 0xF8) | 0x07;
      break;
  }
}
// пример использования PCINT - прерывания на любом пине
// прерывание вызывается при переключении состояния любого пина из группы

// наши обработчики прерываний
ISR(PCINT0_vect) {  // пины 8-13

}
ISR(PCINT1_vect) {  // пины A0-A5

}
ISR(PCINT2_vect) {  // пины 0-7

}

// функция для настройки PCINT
uint8_t attachPCINT(uint8_t pin) {
  if (pin < 8) {            // D0-D7 (PCINT2)
    PCICR |= (1 << PCIE2);
    PCMSK2 |= (1 << pin); return 2; } else if (pin > 13) {    //A0-A5 (PCINT1)
    PCICR |= (1 << PCIE1);
    PCMSK1 |= (1 << pin - 14);
    return 1;
  } else {                  // D8-D13 (PCINT0)
    PCICR |= (1 << PCIE0);
    PCMSK0 |= (1 << pin - 8);
    return 0;
  }
}


// быстрый digitalRead для опроса внутри ISR
// пригодится для проверки конкретного пина
bool pinRead(uint8_t pin) {
  if (pin < 8) {
    return bitRead(PIND, pin);
  } else if (pin < 14) {
    return bitRead(PINB, pin - 8);
  } else if (pin < 20) {
    return bitRead(PINC, pin - 14);
  }
}
// прицепляем аппаратные прерывания напрямую (пин, тип)
void attachInterruptFast(uint8_t num, uint8_t type) {
  switch (num) {					
    case 0:
      EICRA = (EICRA & 0x0C) | type;	// Setup interrupt type
      bitSet(EIMSK, INT0);				// Enable external interrupt
      return;
    case 1:
      EICRA = (EICRA & 0x03) | (type << 2);
      bitSet(EIMSK, INT1);
      return;
  }
}

void detachInterruptFast(uint8_t num) {
  bitClear(EIMSK, num);					// Disable external interrupt
}

// векторы. В них будет прыгать прерывание
ISR(INT0_vect) {

}

ISR(INT1_vect) {

}
// пример работы с юартом
// UART_begin(бод) - запустить
// UART_write(byte) - отправить байт
// UART_available() - проверка на входящий
// UART_read() - прочитать байт
// UART_end() - выключить

void setup() {
  UART_begin(9600);
  UART_write(40);  // отправить байт 40
  UART_write(40);
}

void loop() {
  if (UART_available()) {      // если есть что на приём
    byte data = UART_read();   // прочитать
    UART_write(data);          // отправить обратно
  }
}

// собственно функции
void UART_begin(uint32_t baudrate) {
  uint16_t speed = (F_CPU / (8L * baudrate)) - 1;
  UBRR0H = highByte(speed);
  UBRR0L = lowByte(speed);
  UCSR0A = (1 << U2X0);
  UCSR0B = (1 << TXEN0) | (1 << RXEN0);
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void UART_write(byte data) {
  while (!(UCSR0A & (1 << UDRE0)));
  UDR0 = data;
}

bool UART_available() {
  return (UCSR0A & (1 << RXC0));
}

byte UART_read() {
  byte data = UDR0;
  return data;
}

void UART_end() {
  UCSR0B = 0;
}
// используем минимальный набор для Serial
// стандартный читаемый вывод делает встроенный в ядро Print.h

// ==== класс ====
#include "Print.h"
class Uart : public Print {
  public:
    void begin(uint32_t baudrate) {
      uint16_t speed = (F_CPU / (8L * baudrate)) - 1;
      UBRR0 = speed;
      UCSR0A = (1 << U2X0);
      UCSR0B = (1 << TXEN0) | (1 << RXEN0);
      UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
    }
    void end() {
      UCSR0B = 0;
    }
    size_t write(uint8_t data) {
      while (!(UCSR0A & (1 << UDRE0)));
      UDR0 = data;
    }
    bool available() {
      return (UCSR0A & (1 << RXC0));
    }
    char read() {
      byte data = UDR0;
      return data;
    }
  private:
};
// ==== конец класса ====

Uart uart;

void setup() {
  uart.begin(9600);
  uart.println("Hello ");
  uart.println(123456);
  // также есть
  // uart.end();
  // uart.write();
  // uart.available();
  // uart.read();
}

void loop() {
  
}
// ВНИМАНИЕ! Это просто сон, без отключения АЦП и прочих жрущих блоков
// для полноценного комфортного сна используйте GyverPower

void setup() {
  pinMode(2, 2); // внешняя подтяжка лучше длоя энергосбережения!
  Serial.begin(9600);
  Serial.println("hello!");
  delay(500);
  Serial.println("go to sleep");
  delay(500);
  attachInterrupt(0, wakeUp, LOW);  // вкл прерывание пробуждения
  goToSleep();  // отправка в сон
  delay(1000);  // после сна
  Serial.println("im back in business");  // продолжили работу
}

void wakeUp() {
  detachInterrupt(0);    // откл прерывание пробуждения
  SMCR &= ~ (1 << SE);   // запретили сон
  Serial.println("five more minutes,pls"); // сказали что проснулись
}

void goToSleep() {  
  SMCR |= (1 << SM1); 				// настроили сон как powerDown
  SMCR |= (1 << SE); 				// разрешили сон
  asm volatile ("sleep"); 			// инструкция сна
}

void loop() {
}
// полные аналоги стандартным функциям времени
// пригодится, если работать без Arduino.h
// доступные функции
// необходимо вызвать uptime0Init() при запуске, чтобы всё завелось
void uptime0Init();
unsigned long millis0();
unsigned long micros0();
void delay0(unsigned long ms);
void delayMicroseconds0(unsigned int us);

// ==================== РЕАЛИЗАЦИЯ ==================
#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))
#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)
#define MICROS_MULT (64 / clockCyclesPerMicrosecond())
volatile unsigned long timer0_overflow_count = 0;
volatile unsigned long timer0_millis = 0;
static unsigned char timer0_fract = 0;

void uptime0Init() {
  sei();
  TCCR0A = (1 << WGM01) | (1 << WGM00);
  TCCR0B = (1 << CS01) | (1 << CS00);
  TIMSK0 |= (1 << TOIE0);
} ISR(TIMER0_OVF_vect) {
  timer0_millis += MILLIS_INC;
  timer0_fract += FRACT_INC;
  if (timer0_fract >= FRACT_MAX) {
    timer0_fract -= FRACT_MAX;
    timer0_millis++;
  }
  timer0_overflow_count++;
}
unsigned long millis0() {
  uint8_t oldSREG = SREG;       // запомнинаем были ли включены прерывания
  cli();                // выключаем прерывания
  unsigned long m = timer0_millis;   // перехватить значение
  SREG = oldSREG;           // если прерывания не были включены - не включаем и наоборот
  return m;                // вернуть миллисекунды
}
unsigned long micros0() {
  uint8_t oldSREG = SREG;             // запомнинаем были ли включены прерывания
  cli();                      // выключаем прерывания
  unsigned long m = timer0_overflow_count;     // счет переполнений
  uint8_t t = TCNT0;                 // считать содержимое счетного регистра
  if ((TIFR0 & _BV(TOV0)) && (t < 255))       // инкремент по переполнению
    m++;
  SREG = oldSREG;                 // если прерывания не были включены - не включаем и наоборот
  return (long)(((m << 8) + t) * MICROS_MULT); // вернуть микросекунды
}
void delay0(unsigned long ms) {
  uint32_t start = micros();
  while (ms > 0) { // ведем отсчет
    while ( ms > 0 && (micros() - start) >= 1000) {
      ms--;
      start += 1000;
    }
  }
}
void delayMicroseconds0(unsigned int us) {
#if F_CPU >= 24000000L
  us *= 6; // x6 us, = 7 cycles
  us -= 5; //=2 cycles
#elif F_CPU >= 20000000L
  __asm__ __volatile__ (
    "nop" "\n\t"
    "nop" "\n\t"
    "nop" "\n\t"
    "nop"); //just waiting 4 cycles
  if (us <= 1) return; //  = 3 cycles, (4 when true)
  us = (us << 2) + us; // x5 us, = 7 cycles us -= 7; // 2 cycles #elif F_CPU >= 16000000L
  if (us <= 1) return; //  = 3 cycles, (4 when true)
  us <<= 2; // x4 us, = 4 cycles us -= 5; // = 2 cycles, #elif F_CPU >= 12000000L
  if (us <= 1) return; //  = 3 cycles, (4 when true)
  us = (us << 1) + us; // x3 us, = 5 cycles us -= 5; //2 cycles #elif F_CPU >= 8000000L
  if (us <= 2) return; //  = 3 cycles, (4 when true)
  us <<= 1; //x2 us, = 2 cycles
  us -= 4; // = 2 cycles
#else
  if (us <= 16) return; //= 3 cycles, (4 when true)
  if (us <= 25) return; //= 3 cycles, (4 when true), (must be at least 25 if we want to substract 22) us -= 22; // = 2 cycles us >>= 2; // us div 4, = 4 cycles
#endif
  __asm__ __volatile__ (
    "1: sbiw %0,1" "\n\t" // 2 cycles
    "brne 1b" : "=w" (us) : "0" (us) // 2 cycles
  );
}
// полные аналоги стандартным функциям времени
// пригодится, если работать без Arduino.h
// доступные функции
// необходимо вызвать uptime2Init() при запуске, чтобы всё завелось
void uptime2Init();
unsigned long millis2();
unsigned long micros2();
void delay2(unsigned long ms);
void delayMicroseconds2(unsigned int us);

// =================== РЕАЛИЗАЦИЯ ==================
#define MICROSECONDS_PER_TIMER2_OVERFLOW (clockCyclesToMicroseconds(64 * 256))
#define MILLIS2_INC (MICROSECONDS_PER_TIMER2_OVERFLOW / 1000)
#define FRACT2_INC ((MICROSECONDS_PER_TIMER2_OVERFLOW % 1000) >> 3)
#define FRACT2_MAX (1000 >> 3)
#define MICROS2_MULT (64 / clockCyclesPerMicrosecond())
volatile unsigned long timer2_overflow_count = 0;
volatile unsigned long timer2_millis = 0;
static unsigned char timer2_fract = 0;

void uptime2Init() {
  sei();
  TCCR2A = (1 << WGM20) | (1 << WGM21);
  TCCR2B = 1 << CS22;
  TIMSK2 = 1 << TOIE2;
} ISR(TIMER2_OVF_vect) {
  timer2_millis += MILLIS2_INC;
  timer2_fract += FRACT2_INC;
  if (timer2_fract >= FRACT2_MAX) {
    timer2_fract -= FRACT2_MAX;
    timer2_millis++;
  }
  timer2_overflow_count++;
}
unsigned long millis2() {
  uint8_t oldSREG = SREG;           // запомнинаем были ли включены прерывания
  cli();                            // выключаем прерывания
  unsigned long m = timer2_millis;  // перехватить значение
  SREG = oldSREG;                   // если прерывания не были включены - не включаем и наоборот
  return m;                         // вернуть миллисекунды
}
unsigned long micros2() {
  uint8_t oldSREG = SREG;                   // запомнинаем были ли включены прерывания
  cli();                                    // выключаем прерывания
  unsigned long m = timer2_overflow_count;  // счет переполнений
  uint8_t t = TCNT2;                        // считать содержимое счетного регистра
  if ((TIFR2 & _BV(TOV2)) && (t < 255))     // инкремент по переполнению
    m++;
  SREG = oldSREG;                           // если прерывания не были включены - не включаем и наоборот
  return (long)(((m << 8) + t) * MICROS2_MULT); // вернуть микросекунды
}
void delay2(unsigned long ms) {
  uint32_t start = micros();
  while (ms > 0) { // ведем отсчет
    while ( ms > 0 && (micros() - start) >= 1000) {
      ms--;
      start += 1000;
    }
  }
}
void delayMicroseconds2(unsigned int us) {
#if F_CPU >= 24000000L
  us *= 6; // x6 us, = 7 cycles
  us -= 5; //=2 cycles
#elif F_CPU >= 20000000L
  __asm__ __volatile__ (
    "nop" "\n\t"
    "nop" "\n\t"
    "nop" "\n\t"
    "nop"); //just waiting 4 cycles
  if (us <= 1) return; //  = 3 cycles, (4 when true)
  us = (us << 2) + us; // x5 us, = 7 cycles us -= 7; // 2 cycles #elif F_CPU >= 16000000L
  if (us <= 1) return; //  = 3 cycles, (4 when true)
  us <<= 2; // x4 us, = 4 cycles us -= 5; // = 2 cycles, #elif F_CPU >= 12000000L
  if (us <= 1) return; //  = 3 cycles, (4 when true)
  us = (us << 1) + us; // x3 us, = 5 cycles us -= 5; //2 cycles #elif F_CPU >= 8000000L
  if (us <= 2) return; //  = 3 cycles, (4 when true)
  us <<= 1; //x2 us, = 2 cycles
  us -= 4; // = 2 cycles
#else
  if (us <= 16) return; //= 3 cycles, (4 when true)
  if (us <= 25) return; //= 3 cycles, (4 when true), (must be at least 25 if we want to substract 22) us -= 22; // = 2 cycles us >>= 2; // us div 4, = 4 cycles
#endif
  __asm__ __volatile__ (
    "1: sbiw %0,1" "\n\t" // 2 cycles
    "brne 1b" : "=w" (us) : "0" (us) // 2 cycles
  );
}
// пример чтения установленных фьюз-байтов 

#define LOW_FUSE       (0x0000)
#define LOCK           (0x0001)
#define EXTENDED_FUSE  (0x0002)
#define HIGH_FUSE      (0x0003)

void setup() {
  Serial.begin(9600);
  
  Serial.print("Low:  0x");
  Serial.println(fuse_get(LOW_FUSE_BYTE), HEX);
  
  Serial.print("High: 0x");
  Serial.println(fuse_get(HIGH_FUSE_BYTE), HEX);
  
  Serial.print("Extended:  0x");
  Serial.println(fuse_get(EXTENDED_FUSE_BYTE), HEX);
  
  Serial.print("Lock: 0x");
  Serial.println(fuse_get(LOCK_BYTE), HEX);
  
}

void loop() {  
}


uint8_t fuse_get(uint16_t address) {
  uint8_t data;
  asm volatile
  (
    "sts %[spmreg], %[spmcfg]   \n\t"
    "lpm %[data], Z             \n\t"

    : [data] "=r" (data)
    : [spmreg]"i" (_SFR_MEM_ADDR(SPMCSR)),
    [spmcfg] "r" ((1 << SPMEN) |(1 << BLBSET)),
    "z" (address)
  );
  return data;
}

СТРУКТУРА ПРОГРАММЫ


/*
  пример "чистого" и удобного для работы цикла loop()
  работать так гораздо удобнее, и труднее запутаться
  Пример:
  1: получение показаний с датчика, фильтрация
  2: отработка нажатий кнопок
  3: отрисовка на дисплей
  4: отправка команд на управляющие устройства
  и так далее
*/

void setup() {

}

void loop() {
  task_1();
  task_2();
  task_3();
  task_4();
  // ...
}

void task_1() {
  // какие-то действия, ведущие к одной цели
}
void task_2() {
  // какие-то действия, ведущие к одной цели
}
void task_3() {
  // какие-то действия, ведущие к одной цели
}
void task_4() {
  // какие-то действия, ведущие к одной цели
}
/*
   Данный код демонстрирует переключение режимов работы при помощи кнопки
   Для удобства используется библиотека отработки нажатий кнопки
*/

#define PIN 3        // кнопка подключена сюда (PIN --- КНОПКА --- GND)
#define MODE_AM 5    // количество режимов (от 0 до указанного)

#include "GyverButton.h"
// моя библиотека для более удобной работы с кнопкой
// скачать мождно здесь https://github.com/AlexGyver/GyverLibs

GButton butt1(PIN);  // создаём нашу "кнопку"

byte mode = 0;       // переменная режима

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

void loop() {
  butt1.tick();             // обязательная функция отработки. Должна постоянно опрашиваться
  if (butt1.isPress()) {    // правильная отработка нажатия с защитой от дребезга

    // увеличиваем переменную номера режима. Если вышла за количество режимов - обнуляем
    if (++mode >= MODE_AM) mode = 0;
  }

  // всё переключение в итоге сводится к оператору switch
  switch (mode) {
    case 0: task_0();
      break;
    case 1: task_1();
      break;
    case 2: task_2();
      break;
    case 3: task_3();
      break;
    case 4: task_4();
      break;
  }
}

// наши задачи, внутри функций понятное дело может быть всё что угодно
void task_0() {
  Serial.println("Task 0");
}
void task_1() {
  Serial.println("Task 1");
}
void task_2() {
  Serial.println("Task 2");
}
void task_3() {
  Serial.println("Task 3");
}
void task_4() {
  Serial.println("Task 4");
}
/*
   Данный код демонстрирует переключение режимов работы при помощи кнопки
   Для удобства используется библиотека отработки нажатий кнопки
   В этом варианте примера функции "режимов" вызываются только один раз
*/

#define PIN 3        // кнопка подключена сюда (PIN --- КНОПКА --- GND)
#define MODE_AM 5    // количество режимов (от 0 до указанного)

#include "GyverButton.h"
// моя библиотека для более удобной работы с кнопкой
// скачать мождно здесь https://github.com/AlexGyver/GyverLibs

GButton butt1(PIN);  // создаём нашу "кнопку"

byte mode = 0;       // переменная режима

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

void loop() {
  butt1.tick();             // обязательная функция отработки. Должна постоянно опрашиваться
  if (butt1.isPress()) {    // правильная отработка нажатия с защитой от дребезга

    // увеличиваем переменную номера режма. Если вышла за количество режимов - обнуляем
    if (++mode >= MODE_AM) mode = 0;

    // всё переключение в итоге сводится к оператору switch
    // переключение и вызов происходит только при нажатии!!!
    switch (mode) {
      case 0: task_0();
        break;
      case 1: task_1();
        break;
      case 2: task_2();
        break;
      case 3: task_3();
        break;
      case 4: task_4();
        break;
    }
  }
}

// наши задачи, внутри функций понятное дело может быть всё что угодно
void task_0() {
  Serial.println("Task 0");
}
void task_1() {
  Serial.println("Task 1");
}
void task_2() {
  Serial.println("Task 2");
}
void task_3() {
  Serial.println("Task 3");
}
void task_4() {
  Serial.println("Task 4");
}
Допустим есть у нас задача: переключать режимы по одному и “по кругу”, в простейшем варианте это реализуется вот так:
#define MODE_AMOUNT 5
byte mode = 0;

void nextMode() {
  mode++;  // увеличиваем переменную номера режима
  if (mode >= MODE_AMOUNT) mode = 0;  // закольцовываем
}
// Время выполнения 0.5 мкс
Есть ещё парочка интересных вариантов. Результат не отличается, но сам механизм знать будет полезно:
// второй вариант. Время выполнения 0.5 мкс
if (++mode >= MODE_AMOUNT) mode = 0;  // тут инкремент внесён в условие, получаем более короткую запись

// третий вариант. Время выполнения 5.5 мкс. НЕ ИСПОЛЬЗУЙТЕ ЕГО!
mode = ++mode % MODE_AMOUNT;  // очень интересный вариант, без использования условия! Работает остаток от деления
Очень часто в примерах используется delay(), и это несомненно очень плохо: построить на основе такого примера серьёзный проект практически невозможно. Одним из частых и критичных моментов являются циклы с delay(), это может касаться каких то эффектов со светодиодами, адресными светодиодными лентами, и прочими действиями со счётчиком:
for (int i = 0; i < 30; i++) {
  // например, зажигаем i-ый светодиод
  delay(100);
}
Как переписать такой цикл, чтобы он не блокировал выполнение кода? Очень просто: нужно избавиться и от цикла, и от delay. Введём таймер на millis(), и будем работать по нему:
int counter = 0;      // замена i
uint32_t timer = 0;   // переменная таймера
#define T_PERIOD 100  // период переключения

void loop() {
  
  if (millis() - timer >= T_PERIOD) { // таймер на millis()
    timer = millis(); // сброс
    // действие с counter - наш i-ый светодиод например
    counter++;  // прибавляем счётчик
    if (counter > 30) counter = 0;  // закольцовываем изменение
  }
  
}
Вот собственно и всё! Вместо переменной цикла i у нас теперь свой глобальный счётчик counter, который бегает от 0 до 30 (в этом примере) с периодом 100 мс.

ПОЛЕЗНЫЕ МАКРОСЫ


Часто приходится писать один и тот же цикл for, можно заменить его макросом:
#define FOR_i(from, to) for(int i = (from); i < (to); i++)
Макрос создаст цикл for от from до to и счётчиком i внутри. Пример использования:
// выведет числа от 0 до 9
FOR_i(0, 10) {
  Serial.println(i);
}
Нужны вложенные циклы? Можно сделать макрос с выбором имени переменной
#define FOR(x, from, to) for (int (x) = (from); (x) < (to); (x)++)
И пример с ним:
// работа с двумерным массивом
FOR(i, 0, 10) {
  FOR(j, 0, 3) {
    someArray[i][j] = someValue;
  }
}
//===========================
#define EVERY_MS(x) \
  static uint32_t tmr;\
  bool flag = millis() - tmr >= (x);\
  if (flag) tmr += (x);\
  if (flag)
//===========================
Данный макрос заменяет “таймер на миллис” одной строчкой, без использования библиотек и создания классов! Пользоваться очень просто:
EVERY_MS(100) {
  // ...
  // данный код будет выполняться каждые 100 мс
}
Единственное ограничение: нельзя вызывать макрос больше одного раза в одном и том же блоке кода, это приведёт к ошибке =) То есть вот так нельзя:
void loop() {
  EVERY_MS(100) {
    // ваш код
  }
  EVERY_MS(500) {
    // ваш код
  }
}
Если очень нужна такая конструкция – помещаем каждый вызов в свой блок кода:
void loop() {
  {
    EVERY_MS(100) {
      // ваш код
    }
  }
  
  {
    EVERY_MS(500) {
      // ваш код
    }
  }
  
}
Либо используем блоки кода по условиям или как функцию:
void loop() {
  if (someCondition) {
    EVERY_MS(100) {
      // ваш код
    }
  }
  myAction();
}

void myAction() {
  EVERY_MS(500) {
    // ваш код
  }
}
У – удобно!
При разработке проекта важна отладка, мы делаем её средствами Serial.println(). Чтобы после окончания разработки не убирать из кода все вызовы Serial и не нагружать код условными конструкциями #ifdef DEBUG…. #endif, можно сделать так:
#ifdef DEBUG_ENABLE
#define DEBUG(x) Serial.println(x)
#else
#define DEBUG(x)
#endif
Если DEBUG_ENABLE задефайнен – все вызовы DEBUG() в коде будут заменены на вывод в порт. Если не задефайнен – они будут заменены НИЧЕМ, то есть просто “вырежутся” из кода! Также по DEBUG_ENABLE можно запустить сериал и получить полный контроль над отладкой: если она не нужна – убрали DEBUG_ENABLE и из кода убрался запуск порта и все выводы, что резко сокращает объём занимаемой памяти:
// раздефайнить или задефайнить для использования
//#define DEBUG_ENABLE

#ifdef DEBUG_ENABLE
#define DEBUG(x) Serial.println(x)
#else
#define DEBUG(x)
#endif

void setup() {
#ifdef DEBUG_ENABLE
  Serial.begin(9600);
#endif
}

void loop() {
  DEBUG("kek");
  delay(100);
}
Чтобы полностью остановить программу в каком-то месте, обычно используют замкнутые циклы. Это нужно при отработке каких-то критических ошибок и в других ситуациях. Обычно это делается так:
for(;;);
Можно обернуть в более понятный макрос:
#define FOREVER for(;;)
Теперь в коде на строке FOREVER; код “зависнет”:
// ......
if (criticalError) FOREVER;
// ......

ВРЕМЯ, ТАЙМЕРЫ


// Данный код выполняет действия периодически за указанный период

// Нам нужно задать период таймера В МИЛЛИСЕКУНДАХ
// дней*(24 часов в сутках)*(60 минут в часе)*(60 секунд в минуте)*(1000 миллисекунд в секунде)
// (long) обязательно для больших чисел, иначе не посчитает
// можно посчитать на калькуляторе, но какбэ ардуино и есть калькулятор, пусть считает...
unsigned long period_time = (long)5*24*60*60*1000;

// переменная таймера, максимально большой целочисленный тип (он же uint32_t)
unsigned long my_timer;

void setup() {
  my_timer = millis();   // "сбросить" таймер
}
void loop() {
  if ((long)millis() - my_timer > period_time) {
    my_timer = millis();   // "сбросить" таймер
    // набор функций, который хотим выполнить один раз за период
    // бла бла бла
    // ...
  }
}
// Данный код выполняет действие с периодом PERIOD на время WORK_TIME, эдакий свернизкочастотный ШИМ
// Банально автополив

// Нам нужно задать период таймера В МИЛЛИСЕКУНДАХ
// дней*(24 часов в сутках)*(60 минут в часе)*(60 секунд в минуте)*(1000 миллисекунд в секунде)
// (long) обязательно для больших чисел, иначе не посчитает
// можно посчитать на калькуляторе, но какбэ ардуино и есть калькулятор, пусть считает...
unsigned long period_time = (long)5*24*60*60*1000;

unsigned long work_time = 10000;  // время, на которое ну скажем включится лампочка

#define TIMER_START 0    // 1 - отсчёт периода с момента ВЫКЛЮЧЕНИЯ лампочки, 0 - с ВКЛЮЧЕНИЯ

// переменная таймера, максимально большой целочисленный тип (он же uint32_t)
unsigned long period_timer, work_timer;
boolean work_flag;

void setup() {
  period_timer = millis();   // "сбросить" таймер
}
void loop() {
  if ((long)millis() - period_timer > period_time) {
    period_timer = millis();   // "сбросить" таймер периода
    work_timer = millis();     // сбросить таймер выполнения
    work_flag = true;          // начали выполнение
    // включить лампу, помпу, реле, что угодно
    // банально digitalWrite(пин, HIGH)
  }
  if ( ((long)millis() - work_timer > work_time) && work_flag) {
    work_flag = false;            // сброс флага на выполнение
    // можно сбросить таймер периода ПОСЛЕ выполнения задачи. Подумайте над этим!
    if (TIMER_START) period_timer = millis();
    // выключить лампу, помпу, реле, что угодно
    // банально digitalWrite(пин, LOW)
  }
  if (work_flag) {
    // а вот этот блок кода выполняется всегда, пока мы находимся по времени "внутри" WORK_TIME
  }
}
Таймер, который после срабатывания переключает период на другой, например включаем реле на 10 секунд каждые 60 минут
uint32_t tmr;
bool flag;
#define period1 10*1000L
#define period2 60*60*1000L

void setup() {

}

void loop() {
  if (millis() - tmr >= (flag ? period1 : period2)) {
    tmr = millis();
    flag = !flag;
    // тут можно сделать digitalWrite(pin, flag);
    // для переключения реле
  }
}
/*
   Делаем "параллельное" выполнение нескольких задач
   с разным периодом выполнения
*/

#define PERIOD_1 100    // период первой задачи
#define PERIOD_2 2000   // период второй задачи
#define PERIOD_3 666    // ...

unsigned long timer_1, timer_2, timer_3;

void setup() {

}

void loop() {
  if (millis() - timer_1 > PERIOD_1) {    // условие таймера
    timer_1 = millis();                   // сброс таймера
    
    // выполняем блок №1 каждые PERIOD_1 миллисекунд
  }
  if (millis() - timer_2 > PERIOD_2) {
    timer_2 = millis();
    
    // выполняем блок №2 каждые PERIOD_2 миллисекунд
  }
  if (millis() - timer_3 > PERIOD_3) {
    timer_3 = millis();
    
    // выполняем блок №3 каждые PERIOD_3 миллисекунд
  }
}
/*
   Пример параллельного выполнения нескольких задач
   по таймеру. Библиотеку GyverTimer можно скачать здесь
   https://github.com/AlexGyver/GyverLibs
*/

#include "GyverTimer.h"

// создать миллисекундный таймер, в скобках период в миллисекундах
GTimer myTimer1(MS, 500);
GTimer myTimer2(MS, 600);
GTimer myTimer3(MS, 1000);

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

void loop() {
  if (myTimer1.isReady())
    Serial.println("Timer 1!");

  if (myTimer2.isReady())
    Serial.println("Timer 2!");

  if (myTimer3.isReady())
    Serial.println("Timer 3!");
}
//==== MILLISTIMER MACRO ====
#define EVERY_MS(x) \
  static uint32_t tmr;\
  bool flag = millis() - tmr >= (x);\
  if (flag) tmr += (x);\
  if (flag)
//===========================
Данный макрос заменяет “таймер на миллис” одной строчкой, без использования библиотек и создания классов! Пользоваться очень просто: добавьте указанный выше макрос в самое начало кода и вызывайте его как функцию
EVERY_MS(100) {
  // ...
  // данный код будет выполняться каждые 100 мс
}
Единственное ограничение: нельзя вызывать макрос больше одного раза в одном и том же блоке кода, это приведёт к ошибке =) То есть вот так нельзя:
void loop() {
  EVERY_MS(100) {
    // ваш код
  }
  EVERY_MS(500) {
    // ваш код
  }
}
Если очень нужна такая конструкция – помещаем каждый вызов в свой блок кода:
void loop() {
  {
    EVERY_MS(100) {
      // ваш код
    }
  }
  
  {
    EVERY_MS(500) {
      // ваш код
    }
  }
  
}
Либо используем блоки кода по условиям или как отдельную функцию, которая “оборачивает” макрос:
//===========================
#define EVERY_MS(x) \
  static uint32_t tmr;\
  bool flag = millis() - tmr >= (x);\
  if (flag) tmr = millis();\
  if (flag)
//===========================

void setup() {}

void loop() {
  myAction1();
  myAction2();
}
void myAction1() {
  EVERY_MS(1000) {
    // ваш код
  }
}
void myAction2() {
  EVERY_MS(500) {
    // ваш код
  }
}
uint32_t now = millis();
while (millis () - now < 5000) {
// тут в течение 5000 миллисекунд вертится код
// удобно использовать для всяких калибровок
}
В первом пункте мы разобрали “классический” таймер на millis(), давайте посмотрим ещё один, иногда встречающийся в скетчах:
#define PERIOD_1 2000

void loop() {
  if ( (millis() % PERIOD_1) == 0) {
    delay(1);
    // ваше действие
  }
}
Чем он хорош и чем плох? Хорош тем, что не нужна отдельная переменная типа uint32_t, а также данный таймер не сбивается и без проблем проходит через переполнение millis(). Минусы весьма существенные: операция % выполняется очень долго, также внутри таймера нужен delay(1), иначе таймер может сработать несколько раз в течение одной миллисекунды (пока миллис кратен периоду). Не используйте этот таймер, но знайте, что такой есть.
Недавно я задался вопросом: а можно ли сделать таймер на миллис, который будет корректно обходить переполнение millis() и не сбивать период? Можно, сделал:
#define PERIOD 500
uint32_t timer = 0;
void loop() {
  if (millis() - timer >= PERIOD) {
    // ваше действие
    do {
      timer += PERIOD;
      if (timer < PERIOD) break;  // переполнение uint32_t
    } while (timer < millis() - PERIOD); // защита от пропуска шага
  }
}
Данный таймер имеет механику классического таймера с хранением переменной таймера, а его период всегда кратен PERIOD и не сбивается. Эту конструкцию можно упростить до
#define PERIOD 500
uint32_t timer = 0;
void loop() {
  if (millis() - timer >= PERIOD) {
    // ваше действие
    timer += PERIOD;
  }
}
В этом случае алгоритм получается короче, кратность периодов сохраняется, но теряется защита от пропуска вызова и переполнения millis(). Мои библиотеки GyverTimer и timerMinim были обновлены до этого алгоритма, можете работать с ними.
  // получаем из миллиса часы, минуты и секунды работы программы (часы не ограничены, т.е. аптайм)
  uint32_t sec = millis() / 1000ul;
  int timeHours = (sec / 3600ul);
  int timeMins = (sec % 3600ul) / 60ul;
  int timeSecs = (sec % 3600ul) % 60ul;
  // получаем из миллиса часы, минуты и секунды работы программы. Часы ограничиваем до 23, т.е. режим часов
  uint32_t sec = millis() / 1000ul;
  int timeHours = (sec / 3600ul) % 24;
  int timeMins = (sec % 3600ul) / 60ul;
  int timeSecs = (sec % 3600ul) % 60ul;
// Полезные функции для получения номера дня по дате (день по счёту с 01.01.2000)
// Можно использовать в качестве "миллиса" при подключенном RTC
// Вторая функция получает дату из дня (день по счёту с 01.01.2000)

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

  // смотрим каким днём (по счёту с 01.01.2000) будет 20 июня 2066 года
  Serial.println(daySince2000(20, 6, 2066));

  // временные переменные для работы dayToDate()
  byte day;
  byte month;
  int year;

  // смотрим, в какую дату попадает день 24278 (по счёту с 01.01.2000)
  // данная функция запишет результат в указанные переменные!
  dayToDate(24278, day, month, year);

  Serial.print(day); Serial.print('.');
  Serial.print(month); Serial.print('.');
  Serial.println(year);
}

void loop() {
}

// ============== САМИ ФУНКЦИИ ============
const int daysMonth[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};

// возвращает количество дней с 01.01.2000 (день 1-30/31, месяц 1-12, год 2000-...)
int daySince2000(byte day, byte month, int year) {
  int days = day;                           // + день текущего месяца
  days += daysMonth[month - 1];             // + дней за прошедшие месяцы
  if (month > 2 && year % 4 == 0) days++;   // + високосный
  days += (year - 2000) * 365;              // + предыдущие года
  days += (year - 2000 + 3) / 4;            // + предыдущие високосные года
  return days;
}

// записывает дату дня с номером day2000 в переменные по ссылкам
void dayToDate(int day2000, byte &day, byte &month, int &year) {
  int countDays = day2000;
  year = 0;
  while (countDays > 0) {
    year++;
    countDays -= 365;
    if (year % 4 == 0) countDays--;
  }
  year--;
  day2000 -= year * 365;
  day2000 -= (year + 3) / 4;
  month = 0;
  while (day2000 > daysMonth[month]) month++;
  day = day2000 - daysMonth[month - 1];
  year += 2000;
}

РАБОТА С SERIAL


Парсинг переехал в отдельный урок

ФИЛЬТРЫ ЗНАЧЕНИЙ


По фильтрам есть отдельный урок

/*
  Простейший фильтр: запаздывающий, бегущее среднее, "цифровой фильтр", фильтр низких частот - это всё про него любимого
  Имеет две настройки: постоянную времени FILTER_STEP (миллисекунды), и коэффициент "плавности" FILTER_COEF
  Данный фильтр абсолютно универсален, подходит для сглаживания любого потока данных
  При маленьком значении FILTER_COEF фильтрованное значение будет меняться очень медленно вслед за реальным
  Чем больше FILTER_STEP, тем меньше частота опроса фильтра
  Сгладит любую "гребёнку", шум, ложные срабатывания, резкие вспышки и прочее говно. Пользуюсь им постоянно
*/

#define FILTER_STEP 5
#define FILTER_COEF 0.05

int val;
float val_f = 0.0;
unsigned long filter_timer;

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

void loop() {
  
  if (millis() - filter_timer > FILTER_STEP) {
    filter_timer = millis();    // просто таймер

    // читаем значение (не обязательно с аналога, это может быть ЛЮБОЙ датчик)
    val = analogRead(0);

    // основной алгоритм фильтрации. Внимательно прокрутите его в голове, чтобы понять, как он работает
    val_f = val * FILTER_COEF + val_f * (1 - FILTER_COEF);

    // для примера выведем в порт
    Serial.println(val_f);
  }

}
/*
   "Удобный" фильтр бегущее среднее (низких частот)
   Библиотеку GyverHacks можно скачать здесь
   https://github.com/AlexGyver/GyverLibs
*/

#include "GyverHacks.h"
GFilterRA analog0;    // фильтр назовём analog0

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

  // установка коэффициента фильтрации (0.0... 1.0). Чем меньше, тем плавнее фильтр
  analog0.setCoef(0.01);

  // установка шага фильтрации (мс). Чем меньше, тем резче фильтр
  analog0.setStep(10);
}

void loop() {
  Serial.println(analog0.filteredTime(analogRead(0)));
}
Классический вариант бегущего среднего выглядит так:
// filtered_val - фильтрованное значение
// val - новое значение (с датчика)
// k - коэффициент фильтрации 0.. 1. Обычно около 0.01-0.1 (то бишь float)
filtered_val = filtered_val * (1 - k) + val * k;
Но если раскрыть скобки и “причесать” выражение, получится очень красивая короткая запись. Время выполнения одной операции фильтрации составляет 35 мкс.
filtered_val += (val - filtered_val) * k;
У фильтра “бегущее среднее” не один настраиваемый параметр, как может показаться на первый взгляд. Помимо коэффициента фильтрации k очень большую роль играет время итерации, то есть период вызова фильтра. В реальном коде фильтр вызывается с определённым промежутком времени, чтобы фильтровать шумы. Я обычно настраиваю фильтр вручную по графику, который строится средствами Arduino IDE или программой Serial Port Plotter. Задаюсь периодом итерации и подгоняю k, пока не станет “хорошо”. Но есть и аналитический способ расчёта коэффициента фильтрации (или времени итерации). При выборе значения коэффициента k необходимо отталкиваться от того, какие изменения сигнала нам интересны, а какие мы будем считать за шум. Сделать это можно с помощью следующего выражения: t = dt * (1 / k – 1) где t — период времени, который отделяет слишком быстрые изменения от требуемых; dt — время итерации (период вызова фильтра). Например, если в нашем случае с потенциометром, k = 0,1, а время между двумя измерениями dt = 20 мс, то время t = (1-0.1) * 0,02 / 0.1 = 0,18 сек. То есть все изменения сигнала, которые длятся меньше 0,18 секунд будут подавляться. Во втором случае (при k = 0,3), мы получим t = 0,047 сек. Вникнув в эту связь, можно настроить фильтр, всего лишь глянув на график сырого значения!
/*
  Элементарная реализация среднего арифметического. Сложили NUM_READINGS измерений,
  затем разделили сумму на NUM_READINGS и всё!
  Является "частным случаем" предыдущего фильтра
  Время выполнения примерно равно: 10 значений 50 мкс, 50 значений 92 мкс, 100 значений 146 мкс
*/

#define NUM_READINGS 500
int average;

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

void loop() {
  long sum = 0;                                  // локальная переменная sum
  for (int i = 0; i < NUM_READINGS; i++) {      // согласно количеству усреднений
    sum += analogRead(0);                        // суммируем значения с любого датчика в переменную sum
  }
  average = sum / NUM_READINGS;                  // находим среднее арифметическое, разделив сумму на число измерений
  Serial.println(average);                       // для примера выводим в порт
}
/*
   Готовая функция для вычисления среднего арифметического
   Принимает новые значения, суммирует их в своём массиве
*/
#define NUM_AVER 10       // выборка (из скольки усредняем)

int middleArifm(int newVal) {       // принимает новое значение
  static byte idx = 0;              // индекс
  static int valArray[NUM_AVER];    // массив
  valArray[idx] = newVal;           // пишем каждый раз в новую ячейку
  if (++idx >= NUM_AVER) idx = 0;   // перезаписывая самое старое значение
  long average = 0;                 // обнуляем среднее
  for (int i = 0; i < NUM_AVER; i++) {
    average += valArray[i];         // суммируем
  }
  average /= NUM_AVER;              // делим
  return average;                   // возвращаем
}
/*
   Готовая функция для вычисления среднего арифметического
   Принимает новые значения, суммирует их в своём массиве
*/
// оптимизированный вариант без суммирования массива при каждом вызове
// значения хранятся и отнимаются из переменной суммы
#define NUM_AVER 10   // выборка (из скольки усредняем)

int aver(int val) {
  static int t = 0;
  static int vals[NUM_AVER];
  static int average = 0;

  if (++t >= NUM_AVER) t = 0; // перемотка t
  average -= vals[t];     // вычитаем старое
  average += val;         // прибавляем новое
  vals[t] = val;          // запоминаем в массив
  return (average / NUM_AVER);
}
/*
  Медианный фильтр — довольно простая и интересная штука. Берёт значения и выбирает из них среднее.
  Не усредняет, а именно ВЫБИРАЕТ, отбрасывает все сильно отличющиеся.
  Время выполнения близко к нулю мкс
  Простой пример, чем отличается медианный фильтр от среднего арифметического:
  Возьмём числа 3, 4, 50. Среднее арифметическое даст нам 19. Целью медианного фильтра является фильтрация
  резких скачков, и после фильтрации он даст нам 4, как среднее между 3 и 50, а 50 будет отброшено как скачок.
  В данном скетче реализована фильтрация по трём значениям. Если интересен вариант с фильтрацией более трёх значений,
  то добро пожаловать в исходную статью. Осторожно, жесть. http://tqfp.org/programming/mediannyy-filtr-na-sluzhbe-razrabotchika.html
*/
int val[3];
int val_filter;
byte index;

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

void loop() {
  if (++index > 2) index = 0; // переключаем индекс с 0 до 2 (0, 1, 2, 0, 1, 2…)
  val[index] = analogRead(0); // записываем значение с датчика в массив

  // фильтровать медианным фильтром из 3ёх ПОСЛЕДНИХ измерений
  val_filter = middle_of_3(val[0], val[1], val[2]);

  Serial.println(val_filter); // для примера выводим в порт
}

// медианный фильтр из 3ёх значений
float middle_of_3(float a, float b, float c) {
  int middle;
  if ((a <= b) && (a <= c)) {
    middle = (b <= c) ? b : c;
  }
  else {
    if ((b <= a) && (b <= c)) {
      middle = (a <= c) ? a : c;
    }
    else {
      middle = (a <= b) ? a : b;
    }
  }
  return middle;
}

ЧИСЛА, МАТЕМАТИКА


В этом примере покажу, как разбить число на цифры и поместить в массив:
void setup() {
  Serial.begin(9600);
  long data = 1234567;   // число, которое нужно разбить
  int8_t bytes[10];      // буфер
  byte amount;           // количество цифр в числе
  for (byte i = 0; i < 10; i++) { //>
    bytes[i] = data % 10; // записываем остаток в буфер
    data /= 10;         // "сдвигаем" число
    if (data == 0) {    // если число закончилось
      amount = i;       // запомнили, сколько знаков
      break;
    }
  } // массив bytes хранит цифры числа data в обратном порядке!
  for (int8_t i = amount; i >= 0; i--) {  //>
    Serial.println(bytes[i]);   // выводим
  }
}

void loop() {}
Обратная задача: собрать число из цифр, например – из массива. Удобно при парсинге по одному символу
void setup() {
  Serial.begin(9600);
  byte digits[] = {1, 2, 3, 4, 5, 6, 7};
  long number = 0;
  for (byte i = 0; i < sizeof(digits) / sizeof(digits[0]); i++) {
    number += digits[i];  // пишем следующую цифру
    number *= 10;         // "сдвигаем" число
  }
  number /= 10; // убираем лишнее умножение на 10
  Serial.println(number);
}
По умолчанию в С++ целочисленное деление производится с отбрасыванием дробной части, т.е. 3/4==0 или 5/4==1. Иногда бывает нужно получить округление вверх, для этого можно использовать функцию ceil(), но она работает с float и является очень громоздким решением. Целочисленная операция деления числа a на число b с округлением вверх делается вот так: (a + b - 1) / b.
int val = 1234, val2;
byte b1, b2;

// разбиваем val на байты
b1 = val >> 8;          // старший байт
b2 = val & 0xFF;        // младший байт

val2 = b2 | (b1 << 8);  // склеиваем обратно
// тут val2 == 1234
Может пригодиться для дистанционного управления цветом чего-либо: функция принимает цвет в виде текста в формате RRGGBB, или 0xRRGGBB, или #RRGGBB, и преобразовывает в целочисленный uint32_t. Даю два варианта: для аргумента String и для массива символов. Отличается только описание самого аргумента! Внутри обе функции абсолютно одинаковые.
// char array HEX string to uint32_t
uint32_t HEXtoInt(char *hexValue) {
  byte tens, ones, number1, number2, number3;
  byte s;
  if (hexValue[1] == 'x') s = 2;
  if (hexValue[0] == '#') s = 1;
  
  tens = (hexValue[0+s] < '9') ? hexValue[0+s] - '0' : hexValue[0+s] - '7';
  ones = (hexValue[1+s] < '9') ? hexValue[1+s] - '0' : hexValue[1+s] - '7';
  number1 = (16 * tens) + ones;

  tens = (hexValue[2+s] < '9') ? hexValue[2+s] - '0' : hexValue[2+s] - '7';
  ones = (hexValue[3+s] < '9') ? hexValue[3+s] - '0' : hexValue[3+s] - '7';
  number2 = (16 * tens) + ones;

  tens = (hexValue[4+s] < '9') ? hexValue[4+s] - '0' : hexValue[4+s] - '7';
  ones = (hexValue[5+s] < '9') ? hexValue[5+s] - '0' : hexValue[5+s] - '7';
  number3 = (16 * tens) + ones;

  return ((uint32_t)number1 << 16 | (uint32_t)number2 << 8 | number3 << 0);
}
// String HEX string to uint32_t
uint32_t HEXtoIntS(String hexValue) {
  byte tens, ones, number1, number2, number3;
  byte s;
  if (hexValue[1] == 'x') s = 2;
  if (hexValue[0] == '#') s = 1;
  
  tens = (hexValue[0+s] < '9') ? hexValue[0+s] - '0' : hexValue[0+s] - '7';
  ones = (hexValue[1+s] < '9') ? hexValue[1+s] - '0' : hexValue[1+s] - '7';
  number1 = (16 * tens) + ones;

  tens = (hexValue[2+s] < '9') ? hexValue[2+s] - '0' : hexValue[2+s] - '7';
  ones = (hexValue[3+s] < '9') ? hexValue[3+s] - '0' : hexValue[3+s] - '7';
  number2 = (16 * tens) + ones;

  tens = (hexValue[4+s] < '9') ? hexValue[4+s] - '0' : hexValue[4+s] - '7';
  ones = (hexValue[5+s] < '9') ? hexValue[5+s] - '0' : hexValue[5+s] - '7';
  number3 = (16 * tens) + ones;

  return ((uint32_t)number1 << 16 | (uint32_t)number2 << 8 | number3 << 0);
}
Да, есть встроенная функция strtol(), но её “подключение” сожрёт весь ваш Flash =) Если вдруг нужно, то вот так:
strtol(&string_convert[0], NULL, 16)
Работает только с форматом RRGGBB

МАССИВЫ, БУФЕРЫ


void setup() {
  Serial.begin(9600);
  byte bytes[] = {0, 1, 2, 3, 4, 5, 6, 7};

  // выводим в порт
  for (byte i = 0; i < 8; i++) {
    Serial.print(bytes[i]);
    Serial.print(' ');
  }
  Serial.println();

  // копируем массив в буфер
  byte buf[8];
  for (byte i = 0; i < 8; i++) {
    buf[i] = bytes[i];
  }

  // переписываем наоборот
  for (byte i = 0; i < 8; i++) {
    bytes[i] = buf[7 - i];
  }

  // выводим для проверки
  for (byte i = 0; i < 8; i++) {
    Serial.print(bytes[i]);
    Serial.print(' ');
  }
}
Допустим, нам нужно сравнить каждый элемент массива со всеми остальными (как например в видео про симуляцию эпидемии https://www.youtube.com/watch?v=y3Ei3d7wQv8 ). Можно оптимизировать процесс при помощи диагонального цикла:
// objAmount - размер массива
for (int i = 0; i < objAmount-1; i++) {
  for (int j = i+1; j < objAmount; j++) {
    // сравниваем [i] и [j]
  }
}
Рассмотрим, как хранить в массиве например 5 последних значений с датчика для дальнейшего усреднения. Будем работать с линейным буфером, перед записью нового элемента все предыдущие сдвигаются влево, стирая самый первый элемент, и освобождая место для нового.
#define ARRAY_SIZE 5
byte bytes[ARRAY_SIZE];

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

  // 7 раз "задвинем" в массив случайное число
  // и выведем в порт
  for (byte i = 0; i < 7; i++) {
    updateArray(random(0, 100));
    printArray();
  }

  /*
    Вывод:
    0   0   0   0   7
    0   0   0   7   49
    0   0   7   49  73
    0   7   49  73  58
    7   49  73  58  30
    49  73  58  30  72
    73  58  30  72  44
  */
}

void updateArray(int newVal) {
  for (byte i = 0; i < ARRAY_SIZE - 1; i++) {
    // сдвигаем члены влево
    bytes[i] = bytes[i + 1];
  }
  // пишем новое значение в последний элемент
  bytes[ARRAY_SIZE - 1] = newVal;
}

void printArray() {
  // выводим в порт
  for (byte i = 0; i < ARRAY_SIZE; i++) {
    Serial.print(bytes[i]);
    Serial.print('\t');
  }
  Serial.println();
}

void loop() {}
Рассмотрим, как хранить в массиве например 5 последних значений с датчика для дальнейшего усреднения. Будем работать с циклическим буфером: придётся помнить номер последнего нового элемента. Данный алгоритм лучше, т.к. не приходится  перематывать массив.
#define ARRAY_SIZE 5
byte bytes[ARRAY_SIZE];
byte arrayCounter = 0;  // номер ячейки

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

  // 7 раз "задвинем" в массив случайное число
  // и выведем в порт
  for (byte i = 0; i < 7; i++) { //>
    updateArray(random(0, 100));
    printArray();
  }
  /*
    Вывод:
    7 0 0 0  
    7 49 0 0 0
    7 49 73 0 0
    7 49 73 58 0
    7 49 73 58 30
    72 49 73 58 30
    72 44 73 58 30
  */
}

void updateArray(int newVal) { // пишем новое значение в элемент номер arrayCounter
  bytes[arrayCounter] = newVal;
  arrayCounter++; // прибавляем
  // и зацикливаем
  if (arrayCounter > ARRAY_SIZE - 1) arrayCounter = 0;
}

void printArray() {
  // выводим в порт
  for (byte i = 0; i < ARRAY_SIZE; i++) {
    Serial.print(bytes[i]);
    Serial.print('\t');
  }
  Serial.println();
}

void loop() {}
Самый продвинутый вариант буфера – кольцевой. Данный буфер позволяет хранить набор значений, получать самое крайнее, знать, сколько значений осталось непрочитанными, и “добавлять” новые значения в очередь. Суть состоит в том, что мы запоминаем ячейки начала и конца последовательности данных, и можем обращаться к самому “крайнему” значению, в то же время зная, сколько непрочитанных значений осталось. Такой буфер работает быстрее линейного буфера за счёт отсутствия “перемотки” данных на ячейку назад – здесь все данные сидят в своих ячейках, меняется только их “адрес” – начало и конец буфера, голова и хвост. Такой буфер обычно используется для работы с интерфейсами передачи данных, где всё время что-то читается и добавляется. Пример с готовыми функциями по работе с буфером:
// пример кольцевого буфера для хранения набора данных

#define buffer_SIZE 32    // размер буфера
int buffer[buffer_SIZE];  // сам буфер (массив)
uint8_t buffer_head;      // "голова" буфера
uint8_t buffer_tail;      // "хвост" буфера

void setup() {}

void loop() {}

// запись в буфер
void bufferWrite(int newVal) {
  // положение нового значения в буфере
  uint8_t i = (buffer_head + 1) % buffer_SIZE;

  // если есть местечко
  if (i != buffer_tail) {
    buffer[buffer_head] = newVal; // пишем в буфер
    buffer_head = i;              // двигаем голову
  }
}

// чтение из буфера
int bufferRead() {
  if (buffer_head == buffer_tail) return -1;  // буфер пуст
  int thisVal = buffer[buffer_tail];          // берём с хвоста
  buffer_tail = (buffer_tail + 1) % buffer_SIZE;  // хвост двигаем
  return thisVal;   // возвращаем значение
}

// возвращает крайнее значение без удаления из буфера
// если буфер пуст, вернёт -1
int bufferPeek() {
  return buffer_head != buffer_tail ? buffer[buffer_tail] : -1;
}

// вернёт количество непрочитанных элементов
// если буфер пуст, вернёт -1
int bufferAmount() {
  return ((unsigned int)(buffer_SIZE + buffer_head - buffer_tail)) % buffer_SIZE;
}

// "очистка" буфера
void bufferClear() {
  buffer_head = buffer_tail = 0;
}
Данный вариант отличается от предыдущего более быстрым выполнением (остаток от деления заменён условием)
// пример кольцевого буфера для хранения набора данных
#define buffer_SIZE 32    // размер буфера
int buffer[buffer_SIZE];  // сам буфер (массив)
uint8_t buffer_head;      // "голова" буфера
uint8_t buffer_tail;      // "хвост" буфера

void setup() {}
void loop() {}

// запись в буфер
void bufferWrite(int newVal) {
  // положение нового значения в буфере
  uint8_t i = (buffer_head + 1 >= buffer_SIZE) ? 0 : buffer_head + 1;
  
  // если есть местечко
  if (i != buffer_tail) {
    buffer[buffer_head] = newVal; // пишем в буфер
    buffer_head = i;              // двигаем голову
  }
}

// чтение из буфера
int bufferRead() {
  if (buffer_head == buffer_tail) return -1;  // буфер пуст
  int thisVal = buffer[buffer_tail];          // берём с хвоста
  if (++buffer_tail >= buffer_SIZE) buffer_tail = 0;  // хвост двигаем
  return thisVal;   // возвращаем значение
}

// возвращает крайнее значение без удаления из буфера
// если буфер пуст, вернёт -1
int bufferPeek() {
  return (buffer_head != buffer_tail) ? buffer[buffer_tail] : -1;
}

// вернёт количество непрочитанных элементов
// если буфер пуст, вернёт -1
int bufferAmount() {
  return ((unsigned int)(buffer_SIZE + buffer_head - buffer_tail)) % buffer_SIZE;
}

// "очистка" буфера
void bufferClear() {
  buffer_head = buffer_tail = 0;
}

ПЕРЕДАЧА ПАРАМЕТРОВ


int myArray[] = {100, 30, 890, 645, 251};

void setup() {
  getSecond(myArray);   // результат 30
}

int getSecond(int *intArray) {
  // возвращает второй элемент массива
  return intArray[1];
}

void loop() {
}
В целом, то же самое что предыдущий вариант
int myArray[] = {100, 30, 890, 645, 251};

void setup() {
  getSecond(myArray);   // результат 30
}

int getSecond(int intArray[]) {
  // возвращает второй элемент массива
  return intArray[1];
}

void loop() {
}
int myArray[] = {100, 30, 890, 645, 251};

void setup() {
  uart.begin();
  uart.println(getSecond(&myArray));   // результат 30
}

int getSecond(const void * intArray) {
  const int * Array = (const int *) intArray;
  // возвращает второй элемент массива
  return Array[1];
}

void loop() {
}
struct myStruct {
  byte lol = 210;
  int kek = 2019;
  float cheburek = 0.1;
} testStruct;

void setup() {
  Serial.begin(9600);
  Serial.println(getSecond(&testStruct));   // результат 2019
}

int getSecond(const void * tempStruct) {
  const myStruct * thisStruct = (const myStruct *) tempStruct;
  // возвращает второй элемент структуры 
  return thisStruct->kek;
}

void loop() {
}
struct myStruct {
  byte lol = 210;
  int kek = 2019;
  float cheburek = 0.1;
} testStruct;

void setup() {
  Serial.begin(9600);
  Serial.println(getSecond(testStruct));   // результат 2019
}

int getSecond(myStruct tempStruct) {
  // возвращает второй элемент структуры 
  return tempStruct.kek;
}

void loop() {
}
struct myStruct {
  byte lol = 210;
  int kek = 2019;
  float cheburek = 0.1;
} testStruct;

void setup() {
  Serial.begin(9600);
  Serial.println(getSecond(testStruct));   // результат 2019
}

template‹class T› int getSecond(T& tempStruct) {
  // возвращает второй элемент структуры 
  return tempStruct.kek;
}

void loop() {
}
struct myStruct {
  byte lol = 210;
  int kek = 2019;
  float cheburek = 0.1;
} testStruct;
void setup() {
  Serial.begin(9600);
  Serial.println(getSecond(&testStruct));   // результат 2019
}
template‹class T› int getSecond(T* tempStruct) {
  // возвращает второй элемент структуры 
  return tempStruct->kek;
}
void loop() {
}
int c;
int myArray[] = {100, 30, 890, 645, 251, 645, 821, 325};
int newArray[8];

struct kekstruct {
  byte ass = 10;
  int lol = 15000;
  float tazz = 3.14;
} kek;

void setup() {
  // передаём сам массив и его размер в БАЙТАХ
  c = sumFunction(&myArray, sizeof(myArray));
  rewriteFunction(&myArray, &newArray, sizeof(myArray));
  uartBegin();
  uartPrint(c);
  for (byte i = 0; i ‹ sizeof(newArray) / sizeof(int); i++) {
    uartPrintln(newArray[i]);
  }
  uartPrintln();
  kekstruct kek2;
  //rewriteFunction(&kek, &kek2, sizeof(kek));
  rewrite(kek, kek2);
  uartPrintln(kek2.ass);
  uartPrintln(kek2.lol);
  uartPrintln(kek2.tazz);
}
void loop() {
}

// суммирует массив
int sumFunction(void* intArray, int arrSize) {
  uint8_t* current = reinterpret_cast‹uint8_t*›(intArray);

  // переменная для суммирования
  int sum = 0;
  // находим размер массива, разделив его вес
  // на вес одного элемента (тут у нас int)
  /*arrSize = arrSize / sizeof(int);  */
  for (byte i = 0; i ‹ arrSize; i++) {
    sum += *current;
    current++;
  }
  return sum;
}

// переписывает что угодно через reinterpret_cast
void rewriteFunction(const void* curArray, void* tarArray, int arrSize) {
  uint8_t* target = reinterpret_cast‹uint8_t*›(tarArray);
  const uint8_t* current = reinterpret_cast‹const uint8_t*›(curArray);
  while (arrSize--) {
    *target++ = *current++;
  }
}

ЦВЕТ, СВЕТОДИОДЫ


/*
  Данный код позволяет получить 1023 оттенка цвета с RGB светодиода одним потенциомтером
  алгоритм цвета 1:
  синий максимум, плавно прибавляется зелёный
  зелёный максимум, плавно убавляется синий
  зелёный максимум, плавно прибавляется красный
  красный максимум, плавно убавляется зелёный
*/
// пины подключения. Обратите внимание, это ШИМ пины
#define R_PIN 3
#define G_PIN 5
#define B_PIN 6
byte bright = 100; // яркость, от 0 до 100 (можно повесить на второй потенциометр при желании)
byte R, G, B;
void setup() {
  // настраиваем как выходы
  pinMode(R_PIN, OUTPUT);
  pinMode(G_PIN, OUTPUT);
  pinMode(B_PIN, OUTPUT);
}
void loop() {
  int colorPot = analogRead(0); // получаем значение с потенциометра (0 - 1023)
  // разбиваем диапазон 0 - 1023 на 4 участка, и играемся с цветом согласно текущему значению
  if (colorPot <= 250) { //>
    byte k = map(colorPot, 0, 250, 0, 255);
    R = 0;
    G = k;
    B = 255;
  } else if (colorPot > 250 && colorPot <= 500) { //>
    byte k = map(colorPot, 250, 500, 0, 255);
    R = 0;
    G = 255;
    B = 255 - k;
  } else if (colorPot > 500 && colorPot <= 750) { //>
    byte k = map(colorPot, 500, 750, 0, 255);
    R = k;
    G = 255;
    B = 0;
  } else if (colorPot > 750 && colorPot <= 1023) { //>
    byte k = map(colorPot, 750, 1023, 0, 255);
    R = 255;
    G = 255 - k;
    B = 0;
  }
  // подаём ШИМ на светодиод, учитывая яркость
  analogWrite(R_PIN, (bright * R / 100));
  analogWrite(G_PIN, (bright * G / 100));
  analogWrite(B_PIN, (bright * B / 100));
}
/*
  Данный код позволяет получить 1023 оттенка цвета с RGB светодиода одним потенциомтером
  алгоритм цвета 2
  синий убавляется, зелёный прибавляется
  зелёный убавляется, красный прибавляется
*/
// пины подключения. Обратите внимание, это ШИМ пины
#define R_PIN 3
#define G_PIN 5
#define B_PIN 6
byte bright = 100; // яркость, от 0 до 100 (можно повесить на второй потенциометр при желании)
byte R, G, B;
void setup() {
  // настраиваем как выходы
  pinMode(R_PIN, OUTPUT);
  pinMode(G_PIN, OUTPUT);
  pinMode(B_PIN, OUTPUT);
}
void loop() {
  int colorPot = analogRead(0); // получаем значение с потенциометра (0 - 1023)
  // разбиваем диапазон 0 - 1023 на 2 участка, и играемся с цветом согласно текущему значению
  if (colorPot <= 500) { //>
    byte k = map(colorPot, 0, 500, 0, 255);
    R = 0;
    G = k;
    B = 255 - k;
  } else if (colorPot > 500) {    //>
    byte k = map(colorPot, 500, 1000, 0, 255);
    R = k;
    G = 255 - k;
    B = 0;
  }
  // подаём ШИМ на светодиод, учитывая яркость
  analogWrite(R_PIN, (bright * R / 100));
  analogWrite(G_PIN, (bright * G / 100));
  analogWrite(B_PIN, (bright * B / 100));
}
/*
  Данный код позволяет получить 1023 оттенка цвета с RGB светодиода одним потенциомтером
  алгоритм цвета 3 - радужный
  красный в зелёный через жёлтый
  зелёный в синий через бирюзовый
  синий в краный через фиолетовый
*/
// пины подключения. Обратите внимание, это ШИМ пины
#define R_PIN 3
#define G_PIN 5
#define B_PIN 6
byte bright = 100; // яркость, от 0 до 100 (можно повесить на второй потенциометр при желании)
byte R, G, B;
void setup() {
  // настраиваем как выходы
  pinMode(R_PIN, OUTPUT);
  pinMode(G_PIN, OUTPUT);
  pinMode(B_PIN, OUTPUT);
}
void loop() {
  int colorPot = analogRead(0); // получаем значение с потенциометра (0 - 1023)
  // разбиваем диапазон 0 - 1023 на 2 участка, и играемся с цветом согласно текущему значению
  if (colorPot <= 340) { //>
    byte k = map(colorPot, 0, 340, 0, 255);
    R = 255 - k;
    G = k;
    B = 0;
  } else if (colorPot > 340 && colorPot <= 680) { //>
    byte k = map(colorPot, 340, 680, 0, 255);
    R = 0;
    G = 255 - k;
    B = k;
  } else if (colorPot > 680) {    //>
    byte k = map(colorPot, 680, 1023, 0, 255);
    R = k;
    G = 0;
    B = 255 - k;
  }
  // подаём ШИМ на светодиод, учитывая яркость
  analogWrite(R_PIN, (bright * R / 100));
  analogWrite(G_PIN, (bright * G / 100));
  analogWrite(B_PIN, (bright * B / 100));
}
  // например цвет в HEX
  long val = 0x12ff34, val2;
  byte r, g, b;

  // разбиваем val на байты по цветам RRGGBB
  r = (val >> 16) & 0xFF;
  g = (val >> 8) & 0xFF;
  b = val & 0xFF;

  // склеиваем обратно в long
  val2 = ((long)r << 16) | ((long)g << 8) | b;

  // тут val2 == 0x12ff34
// функция переводит цвет из HSV в RGB (опционально сразу запускает ШИМ)
void HSVtoRGB(uint8_t h, uint8_t s, uint8_t v) {
  float r, g, b;
  
  float H = (float)h / 255;
  float S = (float)s / 255;
  float V = (float)v / 255;
  
  int i = int(H * 6);
  float f = H * 6 - i;
  float p = V * (1 - S);
  float q = V * (1 - f * S);
  float t = V * (1 - (1 - f) * S);

  switch(i % 6){
  case 0: r = V, g = t, b = p; break;
  case 1: r = q, g = V, b = p; break;
  case 2: r = p, g = V, b = t; break;
  case 3: r = p, g = q, b = V; break;
  case 4: r = t, g = p, b = V; break;
  case 5: r = V, g = p, b = q; break;
  }

  // финальные значения для R G B 8 бит
  byte R = r * 255;
  byte G = g * 255;
  byte B = b * 255;
  
  // тут можно отправлять на ШИМ
  analogWrite(3, R);
  analogWrite(5, G);
  analogWrite(6, B);
}
// функция getBrightCRT принимает обычную и возвращает скорректированную 
// по CRT гамме яркость (8 бит) для управления яркостью светодиодов

// CRT таблица
const uint8_t CRTgammaPGM[256] PROGMEM = {
  0,		0,		1,		1,		1,		1,		1,		1,
  1,		1,		1,		1,		1,		1,		1,		1,
  2,		2,		2,		2,		2,		2,		2,		2,
  3,		3,		3,		3,		3,		3,		4,		4,
  4,		4,		4,		5,		5,		5,		5,		6,
  6,		6,		7,		7,		7,		8,		8,		8,
  9,		9,		9,		10,		10,		10,		11,		11,
  12,		12,		12,		13,		13,		14,		14,		15,
  15,		16,		16,		17,		17,		18,		18,		19,
  19,		20,		20,		21,		22,		22,		23,		23,
  24,		25,		25,		26,		26,		27,		28,		28,
  29,		30,		30,		31,		32,		33,		33,		34,
  35,		35,		36,		37,		38,		39,		39,		40,
  41,		42,		43,		43,		44,		45,		46,		47,
  48,		49,		49,		50,		51,		52,		53,		54,
  55,		56,		57,		58,		59,		60,		61,		62,
  63,		64,		65,		66,		67,		68,		69,		70,
  71,		72,		73,		74,		75,		76,		77,		79,
  80,		81,		82,		83,		84,		85,		87,		88,
  89,		90,		91,		93,		94,		95,		96,		98,
  99,		100,	101,	103,	104,	105,	107,	108,
  109,	110,	112,	113,	115,	116,	117,	119,
  120,	121,	123,	124,	126,	127,	129,	130,
  131,	133,	134,	136,	137,	139,	140,	142,
  143,	145,	146,	148,	149,	151,	153,	154,
  156,	157,	159,	161,	162,	164,	165,	167,
  169,	170,	172,	174,	175,	177,	179,	180,
  182,	184,	186,	187,	189,	191,	193,	194,
  196,	198,	200,	202,	203,	205,	207,	209,
  211,	213,	214,	216,	218,	220,	222,	224,
  226,	228,	230,	232,	233,	235,	237,	239,
  241,	243,	245,	247,	249,	251,	253,	255,
};

byte getBrightCRT(byte val) {
  return pgm_read_byte(&(CRTgammaPGM[val]));
}
// то же самое, что выше, но в виде полинома
// выполняется медленнее таблицы, но занимает меньше Flash

byte getBrightCRT(byte val) {
  return ((float)0.0044 * val * val - 0.1589 * val + 2.6983);
}
((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | ((b & 0b11111000) >> 3)
// здесь color565 - int в кодировке цвета RGB565
byte r = (color565 & 0b1111100000000000) >> 8;
byte g = (color565 & 0b0000011111100000) >> 3;
byte b = (color565 & 0b0000000000011111) << 3;

// если нужен uint32_t - можно склеить
uint32_t color888 = ((long)r << 16) | ((long)g << 8) | b;
int HSV565(uint8_t h, uint8_t s, uint8_t v) {
  float r, g, b;

  float H = (float)h / 255;
  float S = (float)s / 255;
  float V = (float)v / 255;

  int i = int(H * 6);
  float f = H * 6 - i;
  float p = V * (1 - S);
  float q = V * (1 - f * S);
  float t = V * (1 - (1 - f) * S);
  switch (i % 6) {
    case 0: r = V, g = t, b = p; break;
    case 1: r = q, g = V, b = p; break;
    case 2: r = p, g = V, b = t; break;
    case 3: r = p, g = q, b = V; break;
    case 4: r = t, g = p, b = V; break;
    case 5: r = V, g = p, b = q; break;
  }
  // финальные значения для R G B 8 бит
  byte R = r * 255;
  byte G = g * 255;
  byte B = b * 255;
  return ((R & 0b11111000) << 8) | ((G & 0b11111100) << 3) | ((B & 0b11111000) >> 3);
}

КНОПКИ


Все возможности кнопки реализованы в написанной мной библиотеке GyverButton, найти можно в паке моих библиотек на GitHub вот по этой ссылке. Там находится описание, здесь приведу пример использования
/*
   Пример использования библиотеки GyverButton, все возможности в одном скетче.
   - Опрос кнопки с программным антидребезгом контактов
   - Отработка нажатия, удерживания отпускания кнопки
   - Отработка одиночного, двойного и тройного нажатия (вынесено отдельно)
   - Отработка любого количества нажатий кнопки (функция возвращает число нажатий)
   - Отработка нажатия и удержания кнопки
   - Настраиваемый таймаут повторного нажатия/удержания
   - Функция изменения значения переменной с заданным шагом и заданным интервалом по времени
*/

#define PIN 3				// кнопка подключена сюда (PIN --- КНОПКА --- GND)

#include "GyverButton.h"
GButton butt1(PIN);
int value = 0;

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

  butt1.setDebounce(50);        // настройка антидребезга (по умолчанию 80 мс)
  butt1.setTimeout(300);        // настройка таймаута на удержание (по умолчанию 500 мс)
  butt1.setIncrStep(2);         // настройка инкремента, может быть отрицательным (по умолчанию 1)
  butt1.setIncrTimeout(500);    // настрйока интервала инкремента (по умолчанию 800 мс)
}

void loop() {
  butt1.tick();  // обязательная функция отработки. Должна постоянно опрашиваться

  if (butt1.isSingle()) Serial.println("Single");       // проверка на один клик
  if (butt1.isDouble()) Serial.println("Double");       // проверка на двойной клик
  if (butt1.isTriple()) Serial.println("Triple");       // проверка на тройной клик

  if (butt1.hasClicks())                                // проверка на наличие нажатий
    Serial.println(butt1.getClicks());                  // получить (и вывести) число нажатий

  if (butt1.isPress()) Serial.println("Press");         // нажатие на кнопку (+ дебаунс)
  if (butt1.isRelease()) Serial.println("Release");     // отпускание кнопки (+ дебаунс)
  if (butt1.isHolded()) Serial.println("Holded");       // проверка на удержание
  //if (butt1.isHold()) Serial.println("Hold");         // возвращает состояние кнопки

  if (butt1.isIncr()) {                                 // если кнопка была удержана (это для инкремента)
    value = butt1.getIncr(value);                       // увеличивать/уменьшать переменную value с шагом и интервалом
    Serial.println(value);      // для примера выведем в порт
  }
}
#define BTN 3   // кнопка подключена сюда (PIN --- КНОПКА --- GND)
boolean btnState, btnFlag;

void setup() {
  Serial.begin(9600);
  pinMode(BTN, INPUT_PULLUP);
}

void loop() {
  btnState = !digitalRead(BTN);  // читаем состояние кнопки с инверсией. 1 - нажата, 0 - нет
  
  if (btnState && !btnFlag) {    // если нажата и была отпущена (btnFlag 0)
    btnFlag = true;              // запомнили что нажата
    Serial.println("press");
  }
  if (!btnState && btnFlag) {    // если отпущена и была нажата (btnFlag 1)
    btnFlag = false;             // запомнили что отпущена
    Serial.println("release");
  }
}
#define BTN 3         // кнопка подключена сюда (PIN --- КНОПКА --- GND)
#define DEBOUNCE 100  // таймаут антидребезга, миллисекунды

boolean btnState, btnFlag;
unsigned long debounceTimer;

void setup() {
  Serial.begin(9600);
  pinMode(BTN, INPUT_PULLUP);
}

void loop() {
  btnState = !digitalRead(BTN);  // читаем состояние кнопки с инверсией. 1 - нажата, 0 - нет

  // если нажата и была отпущена (btnFlag 0) и прошло не менее DEBOUNCE времени
  if (btnState && !btnFlag && (millis() - debounceTimer > DEBOUNCE)) {
    btnFlag = true;              // запомнили что нажата
    debounceTimer = millis();    // запомнили время нажатия
    Serial.println("press");
  }
  if (!btnState && btnFlag) {    // если отпущена и была нажата (btnFlag 1)
    btnFlag = false;             // запомнили что отпущена
    debounceTimer = millis();    // запомнили время отпускания
    Serial.println("release");
  }
}

АНАЛОГОВЫЕ ПИНЫ, НАПРЯЖЕНИЕ


/*
   Скетч для калибровки точного вольтметра и его использование
   КАЛИБРОВКА:
   0) Ставим vol_calibration 1, прошиваем
   1) Запускаем, открываем монитор. Будет выведено реальное значение Vсс в милливольтах
   из расчёта по стандартной константе 1.1
   2) Измеряем вольтметром напряжение на пинах 5V и GND, полученное значение отправляем в порт
   В МИЛЛИВОЛЬТАХ (то есть если у нас 4.54В то отправляем 4540). Новая константа будет рассчитана
   автоматически и запишется во внутреннюю память
   3) Ставим vol_calibration 0, прошиваем
   4) Наслаждайтесь точными измерениями!
   ИСПОЛЬЗОВАНИЕ:
   0) Функция readVCC возвращает ТОЧНОЕ опорное напряжение В МИЛЛИВОЛЬТАХ. В расчётах используем
   не 5 Вольт, а readVсс!
   1) При использовании analogRead() для перевода в вольты пишем:
   float voltage = analogRead(pin) * (readVсс() / 1023.0); - это точный вольтаж В МИЛЛИВОЛЬТАХ
*/

#define vol_calibration 1    // калибровка вольтметра (если работа от АКБ) 1 - включить, 0 - выключить
float my_vcc_const = 1.1;    // начальное значение константы должно быть 1.1

#include "EEPROMex.h"        // библиотека для работы со внутренней памятью ардуино

void setup() {
  Serial.begin(9600);
  if (vol_calibration) calibration();     // калибровка, если разрешена
  my_vcc_const = EEPROM.readFloat(1000);  // считать константу из памяти
}

void loop() {
  /*
    // отображение заряда в процентах по ёмкости! Интерполировано
 вручную по графику разряда ЛИТИЕВОГО аккумулятора
    int volts = readVcc();
    int capacity;
    if (volts > 3870)
    capacity = map(volts, 4200, 3870, 100, 77);
    else if ((volts <= 3870) && (volts > 3750) )
    capacity = map(volts, 3870, 3750, 77, 54);
    else if ((volts <= 3750) && (volts > 3680) )
    capacity = map(volts, 3750, 3680, 54, 31);
    else if ((volts <= 3680) && (volts > 3400) )
    capacity = map(volts, 3680, 3400, 31, 8);
    else if (volts <= 3400)
    capacity = map(volts, 3400, 2600, 8, 0);

    Serial.println(capacity);
  */

}

void calibration() {
  my_vcc_const = 1.1;                                           // начальаня константа калибровки
  Serial.print("Real VCC is: "); Serial.println(readVcc());     // общаемся с пользователем
  Serial.println("Write your VCC (in millivolts)");
  while (Serial.available() == 0); int Vcc = Serial.parseInt(); // напряжение от пользователя
  float real_const = (float)1.1 * Vcc / readVcc();              // расчёт константы
  Serial.print("New voltage constant: "); Serial.println(real_const, 3);
  Serial.println("Set vol_calibration 0, flash and enjoy!");
  EEPROM.writeFloat(1000, real_const);                          // запись в EEPROM
  while (1);                                                    // уйти в бесконечный цикл
}

// функция чтения внутреннего опорного напряжения, универсальная (для всех ардуин)
long readVcc() {
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
  ADMUX = _BV(MUX3) | _BV(MUX2);
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif
  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Start conversion
  while (bit_is_set(ADCSRA, ADSC)); // measuring
  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
  uint8_t high = ADCH; // unlocks both
  long result = (high << 8) | low;

  result = my_vcc_const * 1023 * 1000 / result; // расчёт реального VCC
  return result; // возвращает VCC
}
// отображение заряда в процентах по ёмкости! Интерполировано
// вручную по графику разряда ЛИТИЕВОГО аккумулятора
int volts = analogRead(0) * 5 * (float)0.977;    // несовсем корректно, так как 5 вольт ровно не бывает. Смотри предыдущий пример
int capacity;
if (volts > 3870)
capacity = map(volts, 4200, 3870, 100, 77);
else if ((volts <= 3870) && (volts > 3750) )
capacity = map(volts, 3870, 3750, 77, 54);
else if ((volts <= 3750) && (volts > 3680) )
capacity = map(volts, 3750, 3680, 54, 31);
else if ((volts <= 3680) && (volts > 3400) )
capacity = map(volts, 3680, 3400, 31, 8);
else if (volts <= 3400)
capacity = map(volts, 3400, 2600, 8, 0);

Serial.println(capacity);

РАЗНОЕ


Данный скетч сканирует адресное пространство шины I2C и выводит адреса обнаруженных устройств в последовательный порт
#include "Wire.h"

void setup() {
  Wire.begin();

  Serial.begin(9600);
  while (!Serial);             // Leonardo: wait for serial monitor
  Serial.println("\nI2C Scanner");
}

void loop() {
  byte error, address;
  int nDevices;

  Serial.println("Scanning...");

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  delay(5000);           // wait 5 seconds for next scan
}
Пример вывода с термистора значения в градусах Цельсия, термистор подключен по схеме делителя напряжения с резистором. Я подключал 10к термистор с 10к резистором соответственно, по схеме GND — термистор — A0 — 10к — 5V. В коде предусмотрена настройка коэффициента термистора (берётся из даташита) и базовой температуры (тоже из даташита)
// GND --- термистор --- A0 --- 10к --- 5V
#define THERM A0            // к какому аналоговому пину мы подключены
#define RESIST_10K 10000    // точное сопротивление 10к резистора (Ом)

void setup() {
  Serial.begin(9600);
  //analogReference(EXTERNAL);
}

void loop() {
  Serial.print("Temperature ");
  Serial.print(getThermTemp(analogRead(THERM)));
  Serial.println(" *C");
  delay(1000);
}

// цифры взяты из даташита
#define RESIST_BASE 10000   // сопротивление при TEMP_BASE градусах по Цельсию (Ом)
#define TEMP_BASE 25        // температура, при которой измерено RESIST_BASE (градусов Цельсия)
#define B_COEF 3435         // бета коэффициент термистора (3000-4000)

float getThermTemp(int resistance) {
  float thermistor;
  thermistor = RESIST_10K / ((float)1023 / resistance - 1);
  thermistor /= RESIST_BASE;                        // (R/Ro)
  thermistor = log(thermistor) / B_COEF;            // 1/B * ln(R/Ro)
  thermistor += (float)1.0 / (TEMP_BASE + 273.15);  // + (1/To)
  thermistor = (float)1.0 / thermistor - 273.15;    // инвертируем и конвертируем в градусы по Цельсию
  return thermistor;
}
guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии