Найти в Дзене
Про технику

Счетчик моточасов на Arduino. Меняем масло не по пробегу, а по часам

Всё просто. Вязло любопытство, сколько километров пробега я намотаю в путешествиях при стандартных 250 моточасах работы двигателя. Да, я собираюсь в трэш-путешествие на убитой и уставшей жиге. Есть у меня такой бэтмобиль, который был однажды куплен для высадки деревьев. Дубы успешно выращены и высажены, и теперь очередь тестирования выносливости отечественного автопрома и проверка собственных сил. Следите за моим путешествием на этом канале, для этого не забудьте подписаться. Залито новое синтетическое моторное масло. Обычно на пробеге 5-7 тыс км я делаю замену. Но как вы знаете, трассовый пробег это нечто иное, он обычно существенно больше, при тех же моточасах, чем городской. Износ масла при трассовом пробеге меньше, проехать в километрах до замены масла можно больше. Для учета моточасов разработал простейшую схему и прошивку на Ардуино Нано. С чем я здесь с вами и поделюсь. Счетчик работает без каких либо датчиков. и лишних подключений в проводку. Устройство можно подключить прямо

Всё просто. Вязло любопытство, сколько километров пробега я намотаю в путешествиях при стандартных 250 моточасах работы двигателя.

Да, я собираюсь в трэш-путешествие на убитой и уставшей жиге. Есть у меня такой бэтмобиль, который был однажды куплен для высадки деревьев. Дубы успешно выращены и высажены, и теперь очередь тестирования выносливости отечественного автопрома и проверка собственных сил. Следите за моим путешествием на этом канале, для этого не забудьте подписаться.

Залито новое синтетическое моторное масло. Обычно на пробеге 5-7 тыс км я делаю замену. Но как вы знаете, трассовый пробег это нечто иное, он обычно существенно больше, при тех же моточасах, чем городской. Износ масла при трассовом пробеге меньше, проехать в километрах до замены масла можно больше.

Для учета моточасов разработал простейшую схему и прошивку на Ардуино Нано. С чем я здесь с вами и поделюсь.

Счетчик работает без каких либо датчиков. и лишних подключений в проводку. Устройство можно подключить прямо на клеммы АКБ, разумеется с использованием предохранителя на 1 ампер. Устройство определяет работу мотора по напряжению в бортсети. Логично, что если генератор работает, значит напряжение в сети более 13,5 вольт, а значит устройство запускает счетчик времени моточасов и прибавляет к прошлым сохраненным значениям.

Теперь про функционал и возможности.

1. Устройство имеет четырехстрочный LCD дисплей 20х4, где задействовано 3 строки для вывода информации. Можно подключить LCD 1602, тогда вручную внесите изменения в код, чтобы данные отображались в две строки.

2. Отображение на LCD напряжения в бортсети, время в моточасах, температура в салоне. Температуру можно выкинуть, если вам не требуется данная функция. Тогда дисплея LCD 1602 хватит за глаза.

3. Сохранение в памяти EEPROM ардуины моточасов при каждой остановке двигателя (когда напряжение менее 13.2 вольт), а также при внезапной потере питания. Емкости в 4700 мкФ достаточно, чтобы при потере питания происходила экстренная запись в EEPROM. В моем случае установлен конденсатор на 3300 мкФ и этого тоже достаточно. Устройство можно отключать от АКБ, моточасы буду сохранены в памяти. При подаче питания будет автоматически отображаться последнее сохраненное время.

4. Кнопка для включения подсветки на 5 секунд (когда двигатель заглушен - подсветка всегда выкл, при запущенном ДВС подсветка всегда вкл) и при длительном удержании более 2 сек сброс моточасов + сброс моточасов в EEPROM.

5. Звуковая сигнализация:

а) при подаче питания;

б) при достижении 230 моточасов однократно 10 бипов, далее, при каждом запуске ДВС также однократно, если ранее было достигнуто 230 часов, чтобы вы никогда не забывали сменить масло вовремя. После сброса счетчика кнопкой, сигнал прекращается.

в) при сбросе счетчика кнопкой;

г) при напряжении питания ниже 12.2 вольт, 25 бипов. Это нужно, например, когда вы едите по трассе и у вас отказывает генератор. Фары включены и очень быстро высаживают АКБ. Буквально через пару минут с неработающим генератором будет достигнуто напряжение 12.2 вольт.

Отображение моточасов выполнено с минутами и секундами. Зачем? Чтобы визуально наблюдать нормальную работу. Если отображать только часы, то не очень понятно, работает счетчик или нет.

Схема моточасов на Ардуино Нано:

Схема счетчика моточасов на Arduino Nano без датчиков и тахометра
Схема счетчика моточасов на Arduino Nano без датчиков и тахометра

Устройство упаковал в корпус от старого ADSL модема.

Счетчик моточасов
Счетчик моточасов

Замечания по настройке.

В коде найдите строчку:

float vPin = avg * (4.25f / 1023.0f);       // Напряжение на пине

Значение 4.25, это напряжение на пине Aref. Замерьте его достаточно точным вольтметром, у вас оно будет отличаться, т.к. применяемый линейный стабилизатор на плате ардуины выдает у всех разное напряжение, оно влияет на точность расчетов. Например, если измеренное напряжение будет 4.39 вольт, то впишите это значение, вместо 4.25, после этого залейте скетч в микроконтроллер.

Устройство успешно прошло отладку и все испытания на столе, в том числе на достижение 230 часов работы до предупреждения о скорой замене масла. Также испытал массу всевозможных условий по потере питания и работе при различных напряжениях в бортсети. На данный момент никаких ошибок в алгоритме не обнаружено. Сейчас устройство установлено в машину и вот уже как пару дней катается, испытывает на себе реальные условия.

Скетч для Arduino IDE:

/**************************************************************************
* Скетч: вольтметр + моточасы + термометр + LCD 20x4 + EEPROM
* Платформа: Arduino Nano (5 В)
* Автор: Про Технику
**************************************************************************/
#include <Wire.h> // Библиотека для I2C
#include <LiquidCrystal_I2C.h> // Библиотека LCD 20x4 I2C
#include <OneWire.h> // Протокол OneWire для DS18B20
#include <DallasTemperature.h> // Библиотека для работы с DS18B20
#include <EEPROMWearLevel.h> // EEPROM с выравниванием износа
#include <GyverButton.h> // Удобная работа с кнопками
// Пины
#define PIN_BTN 2                  // Пин кнопки
#define PIN_BUZZ 3                  // Пин пищалки
#define PIN_VOLT    A0                 // Аналоговый пин для замера напряжения
#define PIN_DS18B20 11                 // Пин датчика температуры DS18B20
// Делитель напряжения (R1 между VIN и A0, R2 между A0 и GND)
const float R1 = 430000.0;
const float R2 = 100000.0;
// Пороговые значения напряжения (в вольтах)
const float V_HIGH         = 13.5;//напряжение при котором запускается счетчик моточасов и подсветка
const float V_LOW          = 13.2;//напряжение при котором сохраняем EEPROM
const float V_BEEP         = 12.7;//напряжение при котром начинает срабатывать сигнал счетчика превышения 230 моточасов, т.е. при каждом включении зажигания один раз
const float V_CRITICAL_LOW = 12.2;//напряжение при котром срабатывает звуковой сигнал низкого напряжения
// Временные константы (мс)
const unsigned long BACKLIGHT_TIME       = 5000UL;   // Время работы подсветки после нажатия кнопки
const unsigned long HOLD_TIME            = 2000UL;   // Удержание кнопки для сброса
const unsigned long LOW_VOLT_TIME        = 10000UL;  // Задержка перед тревогой по низкому напряжению
const unsigned long DISPLAY_REFRESH_TIME = 200UL;    // Интервал обновления дисплея
const unsigned long TEMP_REQUEST_INTERVAL = 1000UL;  // Частота запроса температуры
// Инициализация дисплея
LiquidCrystal_I2C lcd(0x27, 20, 4);
// Работа с температурным датчиком
OneWire oneWire(PIN_DS18B20);
DallasTemperature sensors(&oneWire);
// Работа с кнопкой
GButton button(PIN_BTN);
// EEPROM: версия, число ячеек и индекс хранения
#define EEPROM_LAYOUT_VERSION 1
#define EEPROM_CELLS 1
#define EEPROM_IDX_SECONDS 0
// Переменные
volatile unsigned long lastISRTime = 0;     // Последнее срабатывание прерывания
uint32_t motorSeconds = 0;                  // Счётчик моточасов (в секундах)
bool flag230 = false;                       // Флаг превышения 230 моточасов
unsigned long backlightUntil = 0;           // Время выключения подсветки
unsigned long lowVoltStartTime = 0;         // Время начала низкого напряжения
bool lowVoltAlerted = false;                // Флаг тревоги по низкому напряжению
unsigned long lastDisplayTime = 0;          // Время последнего обновления дисплея
unsigned long lastTempRequest = 0;          // Время последнего запроса температуры
float lastTempC = NAN;                      // Последняя измеренная температура
// Управление звуком
enum { BEEP_IDLE, BEEP_ON, BEEP_PAUSE } beepState = BEEP_IDLE;
uint8_t beepCount = 0;
uint16_t beepOnMs = 0, beepPauseMs = 0;
unsigned long beepTimer = 0;
// Переменные для дисплея
float lastVoltage = -1;
uint32_t lastMotorDisplay = 0;
float lastTempDisplay = -1000;
void handleISR() {
unsigned long t = micros();               // Защита от дребезга (для внешнего использования)
if (t - lastISRTime < 50000UL) return;
lastISRTime = t;
}
void setup() {
Wire.begin();                             // Инициализация I2C
lcd.init();                               // Инициализация LCD
lcd.backlight();                          // Включаем подсветку
lcd.clear();
sensors.begin();                          // Старт датчика температуры
sensors.setWaitForConversion(false);      // Не ждём завершения измерения
sensors.requestTemperatures();            // Старт измерения
lastTempRequest = millis();               // Запоминаем время
pinMode(PIN_BTN, INPUT_PULLUP);           // Кнопка с подтяжкой
button.setDebounce(50);
button.setTimeout(HOLD_TIME);
button.setClickTimeout(500);
button.setType(HIGH_PULL);
button.setDirection(NORM_OPEN);
attachInterrupt(digitalPinToInterrupt(PIN_BTN), handleISR, CHANGE);
pinMode(PIN_BUZZ, OUTPUT);
noTone(PIN_BUZZ);
startBeep(3, 100, 100);                   // Приветствие звуком
EEPROMwl.begin(EEPROM_LAYOUT_VERSION, EEPROM_CELLS);
EEPROMwl.get(EEPROM_IDX_SECONDS, motorSeconds);
}
void loop() {
unsigned long now = millis();
button.tick();                            // Обработка кнопки
handleButtonFlags();                      // Проверка нажатий
serviceBeep();                            // Обслуживание пищалки
if (now - lastTempRequest >= TEMP_REQUEST_INTERVAL) {
lastTempRequest = now;
sensors.requestTemperatures();          // Новый запрос температуры
lastTempC = sensors.getTempCByIndex(0); // Сохраняем результат
}
float voltage = readBatteryVoltage();     // Читаем напряжение
handleMotorHours(voltage);                // Считаем моточасы
handleLowVoltage(voltage);                // Обработка низкого напряжения
if (now - lastDisplayTime >= DISPLAY_REFRESH_TIME) {
lastDisplayTime = now;
updateDisplay(voltage);                 // Обновление дисплея
}
// Включаем подсветку если считаем моточасы или пользователь нажал кнопку
if (voltage > V_HIGH || now < backlightUntil)
lcd.backlight();
else
lcd.noBacklight();
}
// Функция считывания напряжения с делителя
float readBatteryVoltage() {
uint16_t sum = 0;
for (uint8_t i = 0; i < 8; i++) {
sum += analogRead(PIN_VOLT);            // Читаем несколько раз для усреднения
}
float avg = sum / 8.0f;
float vPin = avg * (4.25f / 1023.0f);       // Напряжение на пине
return vPin * (R1 + R2) / R2;              // Пересчёт по формуле делителя
}
// Звук
void startBeep(uint8_t times, uint16_t onMs, uint16_t pauseMs) {
beepCount = times;
beepOnMs = onMs;
beepPauseMs = pauseMs;
beepState = BEEP_ON;
tone(PIN_BUZZ, 2000);
beepTimer = millis();
}
void serviceBeep() {
unsigned long now = millis();
switch (beepState) {
case BEEP_ON:
if (now - beepTimer >= beepOnMs) {
noTone(PIN_BUZZ);
beepState = BEEP_PAUSE;
beepTimer = now;
}
break;
case BEEP_PAUSE:
if (now - beepTimer >= beepPauseMs) {
if (beepCount-- > 1) {
tone(PIN_BUZZ, 2000);
beepState = BEEP_ON;
beepTimer = now;
} else {
beepState = BEEP_IDLE;
}
}
break;
default: break;
}
}
// Обработка кнопки
void handleButtonFlags() {
if (button.isClick()) {
backlightUntil = millis() + BACKLIGHT_TIME; // Включаем подсветку
}
if (button.isHolded()) {
lcd.backlight();
motorSeconds = 0;
EEPROMwl.put(EEPROM_IDX_SECONDS, motorSeconds);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" MOTOCHASY RESET ");
startBeep(5, 100, 100);
backlightUntil = millis() + BACKLIGHT_TIME;
delay(1000);
lcd.clear();
}
}
// Обработка моточасов
void handleMotorHours(float voltage) {
static unsigned long lastSec = 0;
static bool wasHigh = false;
if (voltage > V_HIGH) {
wasHigh = true;
if (millis() - lastSec >= 1000UL) {
lastSec = millis();
motorSeconds++;
}
}
if (voltage < V_LOW && wasHigh) {
wasHigh = false;
EEPROMwl.put(EEPROM_IDX_SECONDS, motorSeconds); // Сохраняем моточасы
}
if (motorSeconds >= 230UL * 3600UL && voltage > V_BEEP && !flag230) {
flag230 = true;
startBeep(10, 200, 100); // Предупреждение о ТО
}
if (voltage < V_BEEP) flag230 = false;
}
// Обработка низкого напряжения
void handleLowVoltage(float voltage) {
unsigned long now = millis();
if (voltage < V_CRITICAL_LOW) {
if (lowVoltStartTime == 0) lowVoltStartTime = now;
else if (!lowVoltAlerted && now - lowVoltStartTime >= LOW_VOLT_TIME) {
lowVoltAlerted = true;
startBeep(25, 200, 200);
}
} else {
lowVoltStartTime = 0;
lowVoltAlerted = false;
}
}
// Обновление дисплея
void updateDisplay(float voltage) {
// Обновляем только если данные изменились
if (abs(voltage - lastVoltage) > 0.05) {
lcd.setCursor(0, 0);
lcd.print("V: ");
lcd.print(voltage, 1);
lcd.print(" V      ");
lastVoltage = voltage;
}
if (motorSeconds != lastMotorDisplay) {
uint16_t hh = motorSeconds / 3600;
uint8_t mm = (motorSeconds % 3600) / 60;
uint8_t ss = motorSeconds % 60;
lcd.setCursor(0, 1);
lcd.print("Tm: ");
if (hh < 10) lcd.print('0'); lcd.print(hh); lcd.print(':');
if (mm < 10) lcd.print('0'); lcd.print(mm); lcd.print(':');
if (ss < 10) lcd.print('0'); lcd.print(ss);
lcd.print("      ");
lastMotorDisplay = motorSeconds;
}
if (abs(lastTempC - lastTempDisplay) > 0.5) {
lcd.setCursor(0, 2);
if (!isnan(lastTempC)) {
lcd.print("Tmp: ");
lcd.print(lastTempC, 1);
lcd.write(223); // символ градуса
lcd.print("C      ");
lastTempDisplay = lastTempC;
}
}
}