Spaces:
Running
Running
Upload 24 files
Browse files- audi5_rabochi.ino +81 -0
- classes.h +247 -0
- config.h +96 -0
- engine_control.cpp +108 -0
- engine_control.h +49 -0
- fuel_learning.cpp +128 -0
- fuel_learning.h +48 -0
- fuel_learning_table.cpp +65 -0
- fuel_learning_table.h +17 -0
- idle_control.cpp +102 -0
- idle_control.h +53 -0
- ignition_learning.cpp +166 -0
- ignition_learning.h +57 -0
- ignition_learning_table.cpp +68 -0
- ignition_learning_table.h +17 -0
- learning_manager.cpp +256 -0
- learning_manager.h +71 -0
- maps.cpp +114 -0
- maps.h +110 -0
- pid_controller.h +119 -0
- settings_manager.cpp +171 -0
- settings_manager.h +35 -0
- structures.h +188 -0
- web_interface.h +297 -0
audi5_rabochi.ino
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include <WiFi.h>
|
2 |
+
#include <WebServer.h>
|
3 |
+
#include <ArduinoJson.h>
|
4 |
+
#include "engine_control.h"
|
5 |
+
#include "idle_control.h"
|
6 |
+
#include "settings_manager.h"
|
7 |
+
#include "config.h"
|
8 |
+
|
9 |
+
// Создаем объекты
|
10 |
+
EngineControl engineControl;
|
11 |
+
IdleController idleControl;
|
12 |
+
SettingsManager settingsManager;
|
13 |
+
WebServer server(80);
|
14 |
+
|
15 |
+
void handleStatus() {
|
16 |
+
StaticJsonDocument<512> doc;
|
17 |
+
|
18 |
+
doc["rpm"] = engineControl.getRPM();
|
19 |
+
doc["map"] = engineControl.getMAP();
|
20 |
+
doc["tps"] = engineControl.getTPS();
|
21 |
+
doc["lambda"] = engineControl.getLambda();
|
22 |
+
doc["knock_level"] = engineControl.knockLevel;
|
23 |
+
doc["knock_events"] = engineControl.getKnockEvents();
|
24 |
+
doc["fuel_correction"] = engineControl.getCurrentFuelCorrection();
|
25 |
+
doc["ignition_correction"] = engineControl.getCurrentIgnitionCorrection();
|
26 |
+
doc["engine_temp"] = engineControl.getEngineTemp();
|
27 |
+
doc["voltage"] = engineControl.getVoltage();
|
28 |
+
doc["learning_enabled"] = engineControl.isLearningEnabled();
|
29 |
+
|
30 |
+
String response;
|
31 |
+
serializeJson(doc, response);
|
32 |
+
server.send(200, "application/json", response);
|
33 |
+
}
|
34 |
+
|
35 |
+
void handleIdleRPM() {
|
36 |
+
if (server.hasArg("rpm")) {
|
37 |
+
int rpm = server.arg("rpm").toInt();
|
38 |
+
if (rpm >= 700 && rpm <= 2000) {
|
39 |
+
idleControl.setTargetRPM(rpm);
|
40 |
+
server.send(200, "text/plain", "OK");
|
41 |
+
return;
|
42 |
+
}
|
43 |
+
}
|
44 |
+
server.send(400, "text/plain", "Invalid RPM value");
|
45 |
+
}
|
46 |
+
|
47 |
+
void setup() {
|
48 |
+
Serial.begin(115200);
|
49 |
+
|
50 |
+
// Устанавливаем связь между объектами
|
51 |
+
idleControl.setEngineControl(&engineControl);
|
52 |
+
|
53 |
+
// Инициализация компонентов
|
54 |
+
engineControl.begin();
|
55 |
+
idleControl.begin();
|
56 |
+
|
57 |
+
// Настройка WiFi
|
58 |
+
WiFi.softAP("ECU_Config", "12345678");
|
59 |
+
|
60 |
+
// Настройка веб-сервера
|
61 |
+
server.on("/status", HTTP_GET, handleStatus);
|
62 |
+
server.on("/idle_rpm", HTTP_POST, handleIdleRPM);
|
63 |
+
|
64 |
+
server.begin();
|
65 |
+
}
|
66 |
+
|
67 |
+
void loop() {
|
68 |
+
// Обработка веб-запросов
|
69 |
+
server.handleClient();
|
70 |
+
|
71 |
+
// Обновление состояния двигателя
|
72 |
+
engineControl.update();
|
73 |
+
|
74 |
+
// Обновление РХХ
|
75 |
+
if (engineControl.getRPM() > 0) {
|
76 |
+
idleControl.update(engineControl.getRPM());
|
77 |
+
}
|
78 |
+
|
79 |
+
// Задержка для стабильной работы
|
80 |
+
delay(10);
|
81 |
+
}
|
classes.h
ADDED
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef CLASSES_H
|
2 |
+
#define CLASSES_H
|
3 |
+
|
4 |
+
#include <Arduino.h>
|
5 |
+
#include <SPI.h>
|
6 |
+
#include <SD.h>
|
7 |
+
#include <WiFi.h>
|
8 |
+
#include <WebServer.h>
|
9 |
+
#include <ArduinoJson.h>
|
10 |
+
#include "structures.h"
|
11 |
+
|
12 |
+
// Класс для работы с SD картой
|
13 |
+
class SDCardManager {
|
14 |
+
public:
|
15 |
+
bool init() {
|
16 |
+
if (!SD.begin(SD_CS_PIN, SPI, SD_SPI_SPEED)) {
|
17 |
+
Serial.println("Ошибка инициализации SD карты");
|
18 |
+
return false;
|
19 |
+
}
|
20 |
+
return true;
|
21 |
+
}
|
22 |
+
|
23 |
+
bool saveData(const char* filename, const void* data, size_t size) {
|
24 |
+
File file = SD.open(filename, FILE_WRITE);
|
25 |
+
if (!file) {
|
26 |
+
Serial.println("Ошибка открытия файла для записи");
|
27 |
+
return false;
|
28 |
+
}
|
29 |
+
|
30 |
+
size_t written = file.write((const uint8_t*)data, size);
|
31 |
+
file.close();
|
32 |
+
|
33 |
+
return written == size;
|
34 |
+
}
|
35 |
+
|
36 |
+
bool loadData(const char* filename, void* data, size_t size) {
|
37 |
+
File file = SD.open(filename, FILE_READ);
|
38 |
+
if (!file) {
|
39 |
+
Serial.println("Ошибка открытия файла для чтения");
|
40 |
+
return false;
|
41 |
+
}
|
42 |
+
|
43 |
+
size_t read = file.read((uint8_t*)data, size);
|
44 |
+
file.close();
|
45 |
+
|
46 |
+
return read == size;
|
47 |
+
}
|
48 |
+
};
|
49 |
+
|
50 |
+
// Класс для работы с PSRAM
|
51 |
+
class PSRAMManager {
|
52 |
+
private:
|
53 |
+
void* psramData;
|
54 |
+
size_t dataSize;
|
55 |
+
bool initialized;
|
56 |
+
|
57 |
+
public:
|
58 |
+
PSRAMManager() : psramData(nullptr), dataSize(0), initialized(false) {}
|
59 |
+
|
60 |
+
bool init() {
|
61 |
+
if (!psramInit()) {
|
62 |
+
Serial.println("Ошибка инициализации PSRAM");
|
63 |
+
return false;
|
64 |
+
}
|
65 |
+
|
66 |
+
// Выделяем память для всех данных
|
67 |
+
dataSize = sizeof(ConfigData) + sizeof(LearningData) + sizeof(RXXLearningData);
|
68 |
+
psramData = ps_malloc(dataSize);
|
69 |
+
|
70 |
+
if (!psramData) {
|
71 |
+
Serial.println("Ошибка выделения памяти в PSRAM");
|
72 |
+
return false;
|
73 |
+
}
|
74 |
+
|
75 |
+
initialized = true;
|
76 |
+
Serial.println("PSRAM инициализирован");
|
77 |
+
return true;
|
78 |
+
}
|
79 |
+
|
80 |
+
bool saveData(const void* data, size_t size, size_t offset) {
|
81 |
+
if (!initialized) return false;
|
82 |
+
|
83 |
+
if (offset + size > dataSize) {
|
84 |
+
Serial.println("Ошибка: превышение размера PSRAM");
|
85 |
+
return false;
|
86 |
+
}
|
87 |
+
|
88 |
+
memcpy((uint8_t*)psramData + offset, data, size);
|
89 |
+
return true;
|
90 |
+
}
|
91 |
+
|
92 |
+
bool loadData(void* data, size_t size, size_t offset) {
|
93 |
+
if (!initialized) return false;
|
94 |
+
|
95 |
+
if (offset + size > dataSize) {
|
96 |
+
Serial.println("Ошибка: превышение размера PSRAM");
|
97 |
+
return false;
|
98 |
+
}
|
99 |
+
|
100 |
+
memcpy(data, (uint8_t*)psramData + offset, size);
|
101 |
+
return true;
|
102 |
+
}
|
103 |
+
|
104 |
+
size_t getFreeMemory() {
|
105 |
+
return ESP.getFreePsram();
|
106 |
+
}
|
107 |
+
|
108 |
+
size_t getTotalMemory() {
|
109 |
+
return ESP.getPsramSize();
|
110 |
+
}
|
111 |
+
};
|
112 |
+
|
113 |
+
// Класс для работы с веб-сервером
|
114 |
+
class WebServerManager {
|
115 |
+
private:
|
116 |
+
WebServer server;
|
117 |
+
PSRAMManager psramManager;
|
118 |
+
|
119 |
+
public:
|
120 |
+
void init() {
|
121 |
+
// Инициализация WiFi
|
122 |
+
WiFi.mode(WIFI_AP_STA);
|
123 |
+
WiFi.softAP(WEB_SSID, WEB_PASSWORD);
|
124 |
+
|
125 |
+
// Настройка маршрутов
|
126 |
+
server.on("/", HTTP_GET, [this]() {
|
127 |
+
server.send(200, "text/html", htmlPage);
|
128 |
+
});
|
129 |
+
|
130 |
+
server.on("/config", HTTP_GET, [this]() {
|
131 |
+
StaticJsonDocument<1024> doc;
|
132 |
+
ConfigData config;
|
133 |
+
if (psramManager.loadData(&config, sizeof(ConfigData), 0)) {
|
134 |
+
doc["injectionTimeMin"] = config.injectionTimeMin;
|
135 |
+
doc["injectionTimeMax"] = config.injectionTimeMax;
|
136 |
+
// ... добавьте остальные поля конфигурации
|
137 |
+
}
|
138 |
+
|
139 |
+
String response;
|
140 |
+
serializeJson(doc, response);
|
141 |
+
server.send(200, "application/json", response);
|
142 |
+
});
|
143 |
+
|
144 |
+
server.on("/config", HTTP_POST, [this]() {
|
145 |
+
StaticJsonDocument<1024> doc;
|
146 |
+
DeserializationError error = deserializeJson(doc, server.arg("plain"));
|
147 |
+
|
148 |
+
if (error) {
|
149 |
+
server.send(400, "text/plain", "Ошибка разбора JSON");
|
150 |
+
return;
|
151 |
+
}
|
152 |
+
|
153 |
+
ConfigData config;
|
154 |
+
// Обновление конфигурации
|
155 |
+
config.injectionTimeMin = doc["injectionTimeMin"];
|
156 |
+
config.injectionTimeMax = doc["injectionTimeMax"];
|
157 |
+
// ... обновите остальные поля конфигу��ации
|
158 |
+
|
159 |
+
if (psramManager.saveData(&config, sizeof(ConfigData), 0)) {
|
160 |
+
server.send(200, "text/plain", "Конфигурация сохранена");
|
161 |
+
} else {
|
162 |
+
server.send(500, "text/plain", "Ошибка сохранения конфигурации");
|
163 |
+
}
|
164 |
+
});
|
165 |
+
|
166 |
+
server.begin();
|
167 |
+
}
|
168 |
+
|
169 |
+
void handleClient() {
|
170 |
+
server.handleClient();
|
171 |
+
}
|
172 |
+
};
|
173 |
+
|
174 |
+
// Класс для управления двигателем
|
175 |
+
class EngineManager {
|
176 |
+
private:
|
177 |
+
float currentRPM;
|
178 |
+
float currentThrottle;
|
179 |
+
float currentTemp;
|
180 |
+
float currentLambda;
|
181 |
+
|
182 |
+
public:
|
183 |
+
void updateSensors(float rpm, float throttle, float temp, float lambda) {
|
184 |
+
currentRPM = rpm;
|
185 |
+
currentThrottle = throttle;
|
186 |
+
currentTemp = temp;
|
187 |
+
currentLambda = lambda;
|
188 |
+
}
|
189 |
+
|
190 |
+
float calculateInjectionTime() {
|
191 |
+
// Базовая логика расчета времени впрыска
|
192 |
+
float baseTime = map(currentRPM, 0, MAX_RPM,
|
193 |
+
configData.injectionTimeMin,
|
194 |
+
configData.injectionTimeMax);
|
195 |
+
|
196 |
+
// Коррекция по температуре
|
197 |
+
if (configData.tempCorrectionEnabled) {
|
198 |
+
baseTime *= getTempCorrection(currentTemp);
|
199 |
+
}
|
200 |
+
|
201 |
+
// Коррекция по лямбде
|
202 |
+
if (configData.lambdaCorrectionEnabled) {
|
203 |
+
baseTime *= getLambdaCorrection(currentLambda);
|
204 |
+
}
|
205 |
+
|
206 |
+
return baseTime;
|
207 |
+
}
|
208 |
+
|
209 |
+
float calculateAdvance() {
|
210 |
+
// Базовая логика расчета УОЗ
|
211 |
+
float baseAdvance = map(currentRPM, 0, MAX_RPM,
|
212 |
+
configData.minAdvance,
|
213 |
+
configData.maxAdvance);
|
214 |
+
|
215 |
+
// Коррекция для холостого хода
|
216 |
+
if (isIdle()) {
|
217 |
+
return getIdleAdvance();
|
218 |
+
}
|
219 |
+
|
220 |
+
return baseAdvance;
|
221 |
+
}
|
222 |
+
|
223 |
+
private:
|
224 |
+
bool isIdle() {
|
225 |
+
return currentRPM >= configData.idleRpmMin &&
|
226 |
+
currentRPM <= configData.idleRpmMax &&
|
227 |
+
currentThrottle < THROTTLE_IDLE_THRESHOLD;
|
228 |
+
}
|
229 |
+
|
230 |
+
float getTempCorrection(float temp) {
|
231 |
+
// Логика коррекции по температуре
|
232 |
+
return 1.0; // Заглушка
|
233 |
+
}
|
234 |
+
|
235 |
+
float getLambdaCorrection(float lambda) {
|
236 |
+
// Логика коррекции по лямбде
|
237 |
+
return 1.0; // Заглушка
|
238 |
+
}
|
239 |
+
|
240 |
+
float getIdleAdvance() {
|
241 |
+
// Логика расчета УОЗ на холостом ходу
|
242 |
+
return map(currentRPM, configData.idleRpmMin, configData.idleRpmMax,
|
243 |
+
configData.idleAdvanceMin, configData.idleAdvanceMax);
|
244 |
+
}
|
245 |
+
};
|
246 |
+
|
247 |
+
#endif // CLASSES_H
|
config.h
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
|
3 |
+
// Пины для датчиков
|
4 |
+
#define HALL_SENSOR_PIN 36
|
5 |
+
#define MAP_SENSOR_PIN 39
|
6 |
+
#define TPS_PIN 34
|
7 |
+
#define LAMBDA_SENSOR_PIN 35
|
8 |
+
#define ENGINE_TEMP_SENSOR_PIN 32
|
9 |
+
#define BATTERY_VOLTAGE_PIN 33
|
10 |
+
#define KNOCK_SENSOR_1_3_PIN 25
|
11 |
+
#define KNOCK_SENSOR_4_5_PIN 26
|
12 |
+
|
13 |
+
// Пины для РХХ
|
14 |
+
#define IDLE_VALVE_PIN_A 27
|
15 |
+
#define IDLE_VALVE_PIN_B 14
|
16 |
+
|
17 |
+
// Пороговые значения
|
18 |
+
#define START_RPM_THRESHOLD 400
|
19 |
+
#define DETONATION_THRESHOLD 0.8
|
20 |
+
|
21 |
+
// Настройки PWM для РХХ
|
22 |
+
#define IDLE_VALVE_PWM_FREQ 1000
|
23 |
+
#define IDLE_VALVE_PWM_RES 8 // 8 bit resolution
|
24 |
+
|
25 |
+
// Пины управления
|
26 |
+
#define FUEL_PUMP_PIN 16 // Изменен с 27
|
27 |
+
#define IGNITION_PIN 17 // Изменен с 14
|
28 |
+
#define INJECTOR_PIN 12
|
29 |
+
|
30 |
+
// Форсунки
|
31 |
+
#define INJECTOR_1_PIN 5
|
32 |
+
#define INJECTOR_2_PIN 18
|
33 |
+
#define INJECTOR_3_PIN 19
|
34 |
+
#define INJECTOR_4_PIN 21
|
35 |
+
#define INJECTOR_5_PIN 22
|
36 |
+
|
37 |
+
// Катушки зажигания
|
38 |
+
#define COIL_1_PIN 23
|
39 |
+
#define COIL_2_PIN 15
|
40 |
+
#define COIL_3_PIN 13
|
41 |
+
#define COIL_4_PIN 12
|
42 |
+
#define COIL_5_PIN 4
|
43 |
+
|
44 |
+
// Настройки WiFi
|
45 |
+
#define WEB_SSID "ECU_Config"
|
46 |
+
#define WEB_PASSWORD "12345678"
|
47 |
+
|
48 |
+
// Константы двигателя
|
49 |
+
#define NUM_CYLINDERS 5
|
50 |
+
#define MAX_RPM 7000
|
51 |
+
#define MIN_ADVANCE 0
|
52 |
+
#define MAX_ADVANCE 40
|
53 |
+
#define MECHANICAL_ADVANCE 40.0
|
54 |
+
#define DWELL_TIME 3000
|
55 |
+
|
56 |
+
// Константы впрыска
|
57 |
+
#define MIN_INJECTION_TIME 1.0
|
58 |
+
#define MAX_INJECTION_TIME 20.0
|
59 |
+
#define INJECTOR_FLOW 200
|
60 |
+
#define FUEL_PRESSURE 3.0
|
61 |
+
|
62 |
+
// Константы лямбда-регулирования
|
63 |
+
#define LAMBDA_TARGET 1.0
|
64 |
+
#define LAMBDA_CORRECTION_MIN 0.8
|
65 |
+
#define LAMBDA_CORRECTION_MAX 1.2
|
66 |
+
#define LAMBDA_CORRECTION_STEP 0.01
|
67 |
+
|
68 |
+
// Температурные константы
|
69 |
+
#define COLD_START_TEMP 20.0
|
70 |
+
#define HOT_ENGINE_TEMP 90.0
|
71 |
+
#define TEMP_CORRECTION_MAX 1.5
|
72 |
+
|
73 |
+
// Структура конфигурации
|
74 |
+
struct ConfigData {
|
75 |
+
// Базовые таблицы
|
76 |
+
float fuelTable[16][16]; // Базовая таблица топливоподачи
|
77 |
+
float ignitionTable[16][16]; // Базовая таблица УОЗ
|
78 |
+
|
79 |
+
// Таблицы обучения
|
80 |
+
float fuelLearning[16][16]; // Таблица коррекции топливоподачи
|
81 |
+
float ignitionLearning[16][16]; // Таблица коррекции УОЗ
|
82 |
+
|
83 |
+
// Настройки холостого хода
|
84 |
+
float idleTargetRpm; // Целевые обороты ХХ
|
85 |
+
float idleKp; // Коэффициент P для ПИД
|
86 |
+
float idleKi; // Коэффициент I для ПИД
|
87 |
+
float idleKd; // Коэффициент D для ПИД
|
88 |
+
|
89 |
+
// Контрольная сумма
|
90 |
+
uint32_t checksum; // Контрольная сумма конфигурации
|
91 |
+
};
|
92 |
+
|
93 |
+
// Функции конфигурации
|
94 |
+
bool saveConfig(ConfigData* config);
|
95 |
+
bool loadConfig(ConfigData* config);
|
96 |
+
uint32_t calculateConfigChecksum(ConfigData* config);
|
engine_control.cpp
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "engine_control.h"
|
2 |
+
#include "config.h"
|
3 |
+
#include <Arduino.h>
|
4 |
+
|
5 |
+
// Вспомогательные функции для преобразования значений
|
6 |
+
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
|
7 |
+
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
8 |
+
}
|
9 |
+
|
10 |
+
EngineControl::EngineControl() :
|
11 |
+
rpm(0),
|
12 |
+
map(0),
|
13 |
+
tps(0),
|
14 |
+
lambda(0),
|
15 |
+
knockLevel(0),
|
16 |
+
engineTemp(0),
|
17 |
+
voltage(0),
|
18 |
+
learningEnabled(false),
|
19 |
+
knockEvents(0),
|
20 |
+
currentFuelCorrection(1.0),
|
21 |
+
currentIgnitionCorrection(0.0)
|
22 |
+
{
|
23 |
+
}
|
24 |
+
|
25 |
+
void EngineControl::begin() {
|
26 |
+
// Настройка пинов
|
27 |
+
pinMode(HALL_SENSOR_PIN, INPUT);
|
28 |
+
pinMode(MAP_SENSOR_PIN, INPUT);
|
29 |
+
pinMode(TPS_PIN, INPUT);
|
30 |
+
pinMode(LAMBDA_SENSOR_PIN, INPUT);
|
31 |
+
pinMode(ENGINE_TEMP_SENSOR_PIN, INPUT);
|
32 |
+
pinMode(BATTERY_VOLTAGE_PIN, INPUT);
|
33 |
+
pinMode(KNOCK_SENSOR_1_3_PIN, INPUT);
|
34 |
+
pinMode(KNOCK_SENSOR_4_5_PIN, INPUT);
|
35 |
+
|
36 |
+
// Загрузка таблиц обучения
|
37 |
+
fuelLearning.load();
|
38 |
+
ignitionLearning.load();
|
39 |
+
}
|
40 |
+
|
41 |
+
void EngineControl::update() {
|
42 |
+
// Чтение сенсоров
|
43 |
+
rpm = analogRead(HALL_SENSOR_PIN);
|
44 |
+
map = analogRead(MAP_SENSOR_PIN);
|
45 |
+
tps = analogRead(TPS_PIN);
|
46 |
+
lambda = analogRead(LAMBDA_SENSOR_PIN);
|
47 |
+
engineTemp = analogRead(ENGINE_TEMP_SENSOR_PIN);
|
48 |
+
voltage = analogRead(BATTERY_VOLTAGE_PIN);
|
49 |
+
|
50 |
+
// Преобразование значений АЦП
|
51 |
+
rpm = mapf(rpm, 0, 4095, 0, 7000);
|
52 |
+
map = mapf(map, 0, 4095, 0, 100);
|
53 |
+
tps = mapf(tps, 0, 4095, 0, 100);
|
54 |
+
lambda = mapf(lambda, 0, 4095, 0.7, 1.3);
|
55 |
+
engineTemp = mapf(engineTemp, 0, 4095, -40, 150);
|
56 |
+
voltage = mapf(voltage, 0, 4095, 0, 15);
|
57 |
+
|
58 |
+
if (learningEnabled && rpm > START_RPM_THRESHOLD) {
|
59 |
+
float engineLoad = map; // В данном случае используем MAP как нагрузку
|
60 |
+
|
61 |
+
// Обучение топливоподачи по лямбда-зонду
|
62 |
+
if (lambda < 0.98) {
|
63 |
+
fuelLearning.update(engineLoad, rpm, 0.98);
|
64 |
+
} else if (lambda > 1.02) {
|
65 |
+
fuelLearning.update(engineLoad, rpm, 1.02);
|
66 |
+
}
|
67 |
+
|
68 |
+
// Чтение датчиков детонации
|
69 |
+
float knock1 = analogRead(KNOCK_SENSOR_1_3_PIN);
|
70 |
+
float knock2 = analogRead(KNOCK_SENSOR_4_5_PIN);
|
71 |
+
knockLevel = max(knock1, knock2);
|
72 |
+
|
73 |
+
// Обучение УОЗ по детонации
|
74 |
+
ignitionLearning.learn(engineLoad, rpm, knockLevel);
|
75 |
+
|
76 |
+
// Если была детонация, увеличиваем счетчик
|
77 |
+
if (knockLevel > DETONATION_THRESHOLD) {
|
78 |
+
knockEvents++;
|
79 |
+
}
|
80 |
+
}
|
81 |
+
|
82 |
+
// Получение текущих коррекций
|
83 |
+
float engineLoad = map;
|
84 |
+
currentFuelCorrection = fuelLearning.getCorrection(engineLoad, rpm);
|
85 |
+
currentIgnitionCorrection = ignitionLearning.getCorrection(engineLoad, rpm);
|
86 |
+
}
|
87 |
+
|
88 |
+
float EngineControl::getLearningProgress() const {
|
89 |
+
return (fuelLearning.getProgress() + ignitionLearning.getProgress()) / 2.0;
|
90 |
+
}
|
91 |
+
|
92 |
+
float EngineControl::getIgnitionAdvance() {
|
93 |
+
float engineLoad = map;
|
94 |
+
float baseAdvance = 10.0; // Базовый УОЗ
|
95 |
+
|
96 |
+
// Добавляем коррекцию от обучения
|
97 |
+
float ignitionCorr = ignitionLearning.getCorrection(engineLoad, rpm);
|
98 |
+
|
99 |
+
return baseAdvance + ignitionCorr;
|
100 |
+
}
|
101 |
+
|
102 |
+
bool EngineControl::saveLearningTables() {
|
103 |
+
return fuelLearning.save() && ignitionLearning.save();
|
104 |
+
}
|
105 |
+
|
106 |
+
bool EngineControl::loadLearningTables() {
|
107 |
+
return fuelLearning.load() && ignitionLearning.load();
|
108 |
+
}
|
engine_control.h
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
#include "Arduino.h"
|
3 |
+
#include "structures.h"
|
4 |
+
#include "fuel_learning_table.h"
|
5 |
+
#include "ignition_learning_table.h"
|
6 |
+
|
7 |
+
class EngineControl {
|
8 |
+
private:
|
9 |
+
float rpm;
|
10 |
+
float map;
|
11 |
+
float tps;
|
12 |
+
float lambda;
|
13 |
+
float engineTemp;
|
14 |
+
float voltage;
|
15 |
+
|
16 |
+
FuelLearningTable fuelLearning;
|
17 |
+
IgnitionLearningTable ignitionLearning;
|
18 |
+
bool learningEnabled;
|
19 |
+
uint32_t knockEvents;
|
20 |
+
float currentFuelCorrection;
|
21 |
+
float currentIgnitionCorrection;
|
22 |
+
|
23 |
+
public:
|
24 |
+
float knockLevel; // Перемещено в public для доступа из основного кода
|
25 |
+
|
26 |
+
EngineControl();
|
27 |
+
void begin();
|
28 |
+
void update();
|
29 |
+
|
30 |
+
// Геттеры
|
31 |
+
float getRPM() const { return rpm; }
|
32 |
+
float getMAP() const { return map; }
|
33 |
+
float getTPS() const { return tps; }
|
34 |
+
float getLambda() const { return lambda; }
|
35 |
+
float getEngineTemp() const { return engineTemp; }
|
36 |
+
float getVoltage() const { return voltage; }
|
37 |
+
float getLearningProgress() const;
|
38 |
+
float getIgnitionAdvance();
|
39 |
+
uint32_t getKnockEvents() const { return knockEvents; }
|
40 |
+
float getCurrentFuelCorrection() const { return currentFuelCorrection; }
|
41 |
+
float getCurrentIgnitionCorrection() const { return currentIgnitionCorrection; }
|
42 |
+
|
43 |
+
// Управление обучением
|
44 |
+
void resetLearning() { fuelLearning.reset(); ignitionLearning.reset(); }
|
45 |
+
bool saveLearningTables();
|
46 |
+
bool loadLearningTables();
|
47 |
+
void setLearningEnabled(bool enabled) { learningEnabled = enabled; }
|
48 |
+
bool isLearningEnabled() const { return learningEnabled; }
|
49 |
+
};
|
fuel_learning.cpp
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "fuel_learning.h"
|
2 |
+
#include <Preferences.h>
|
3 |
+
|
4 |
+
FuelLearningTable::FuelLearningTable() :
|
5 |
+
learningRate(0.01),
|
6 |
+
maxCorrection(1.2),
|
7 |
+
minCorrection(0.8)
|
8 |
+
{
|
9 |
+
// Инициализация точек нагрузки (кПа)
|
10 |
+
for (int i = 0; i < LOAD_POINTS; i++) {
|
11 |
+
loadPoints[i] = 20.0 + (i * 6.0); // 20-110 кПа
|
12 |
+
}
|
13 |
+
|
14 |
+
// Инициализация точек оборотов
|
15 |
+
for (int i = 0; i < RPM_POINTS; i++) {
|
16 |
+
rpmPoints[i] = 800.0 + (i * 400.0); // 800-6800 RPM
|
17 |
+
}
|
18 |
+
|
19 |
+
// Инициализация ячеек коррекции
|
20 |
+
reset();
|
21 |
+
}
|
22 |
+
|
23 |
+
void FuelLearningTable::begin() {
|
24 |
+
load(); // Загрузка сохраненных данных
|
25 |
+
}
|
26 |
+
|
27 |
+
void FuelLearningTable::reset() {
|
28 |
+
for (int i = 0; i < LOAD_POINTS; i++) {
|
29 |
+
for (int j = 0; j < RPM_POINTS; j++) {
|
30 |
+
cells[i][j] = 1.0; // Нейтральная коррекция
|
31 |
+
}
|
32 |
+
}
|
33 |
+
}
|
34 |
+
|
35 |
+
int FuelLearningTable::findNearestIndex(float value, const float* points, int size) {
|
36 |
+
if (value <= points[0]) return 0;
|
37 |
+
if (value >= points[size-1]) return size - 2;
|
38 |
+
|
39 |
+
for (int i = 0; i < size - 1; i++) {
|
40 |
+
if (value >= points[i] && value < points[i+1]) {
|
41 |
+
return i;
|
42 |
+
}
|
43 |
+
}
|
44 |
+
return 0;
|
45 |
+
}
|
46 |
+
|
47 |
+
float FuelLearningTable::interpolate2D(float load, float rpm) {
|
48 |
+
// Находим ближайшие индексы
|
49 |
+
int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
|
50 |
+
int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
|
51 |
+
|
52 |
+
// Находим веса для интерполяции
|
53 |
+
float loadWeight = (load - loadPoints[loadIdx]) /
|
54 |
+
(loadPoints[loadIdx+1] - loadPoints[loadIdx]);
|
55 |
+
float rpmWeight = (rpm - rpmPoints[rpmIdx]) /
|
56 |
+
(rpmPoints[rpmIdx+1] - rpmPoints[rpmIdx]);
|
57 |
+
|
58 |
+
// Билинейная интерполяция
|
59 |
+
float c00 = cells[loadIdx][rpmIdx];
|
60 |
+
float c10 = cells[loadIdx+1][rpmIdx];
|
61 |
+
float c01 = cells[loadIdx][rpmIdx+1];
|
62 |
+
float c11 = cells[loadIdx+1][rpmIdx+1];
|
63 |
+
|
64 |
+
float c0 = c00 * (1 - loadWeight) + c10 * loadWeight;
|
65 |
+
float c1 = c01 * (1 - loadWeight) + c11 * loadWeight;
|
66 |
+
|
67 |
+
return c0 * (1 - rpmWeight) + c1 * rpmWeight;
|
68 |
+
}
|
69 |
+
|
70 |
+
void FuelLearningTable::updateCell(int loadIdx, int rpmIdx, float correction) {
|
71 |
+
// Применяем коррекцию с учетом скорости обучения
|
72 |
+
cells[loadIdx][rpmIdx] += correction * learningRate;
|
73 |
+
|
74 |
+
// Ограничиваем значение коррекции
|
75 |
+
cells[loadIdx][rpmIdx] = constrain(cells[loadIdx][rpmIdx],
|
76 |
+
minCorrection, maxCorrection);
|
77 |
+
}
|
78 |
+
|
79 |
+
float FuelLearningTable::getCorrection(float load, float rpm) {
|
80 |
+
return interpolate2D(load, rpm);
|
81 |
+
}
|
82 |
+
|
83 |
+
void FuelLearningTable::learn(float load, float rpm, float lambdaError) {
|
84 |
+
// Находим ближайшие ячейки
|
85 |
+
int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
|
86 |
+
int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
|
87 |
+
|
88 |
+
// Обновляем основную ячейку
|
89 |
+
updateCell(loadIdx, rpmIdx, lambdaError);
|
90 |
+
|
91 |
+
// Обновляем соседние ячейки с меньшим весом
|
92 |
+
if (loadIdx > 0) {
|
93 |
+
updateCell(loadIdx-1, rpmIdx, lambdaError * 0.5);
|
94 |
+
}
|
95 |
+
if (loadIdx < LOAD_POINTS-1) {
|
96 |
+
updateCell(loadIdx+1, rpmIdx, lambdaError * 0.5);
|
97 |
+
}
|
98 |
+
if (rpmIdx > 0) {
|
99 |
+
updateCell(loadIdx, rpmIdx-1, lambdaError * 0.5);
|
100 |
+
}
|
101 |
+
if (rpmIdx < RPM_POINTS-1) {
|
102 |
+
updateCell(loadIdx, rpmIdx+1, lambdaError * 0.5);
|
103 |
+
}
|
104 |
+
}
|
105 |
+
|
106 |
+
bool FuelLearningTable::save() {
|
107 |
+
Preferences prefs;
|
108 |
+
prefs.begin("fuel_learn", false);
|
109 |
+
|
110 |
+
// Сохраняем таблицу коррекции
|
111 |
+
size_t written = prefs.putBytes("cells", cells,
|
112 |
+
sizeof(float) * LOAD_POINTS * RPM_POINTS);
|
113 |
+
|
114 |
+
prefs.end();
|
115 |
+
return written == sizeof(float) * LOAD_POINTS * RPM_POINTS;
|
116 |
+
}
|
117 |
+
|
118 |
+
bool FuelLearningTable::load() {
|
119 |
+
Preferences prefs;
|
120 |
+
prefs.begin("fuel_learn", true);
|
121 |
+
|
122 |
+
// Загружаем таблицу коррекции
|
123 |
+
size_t readSize = prefs.getBytes("cells", cells,
|
124 |
+
sizeof(float) * LOAD_POINTS * RPM_POINTS);
|
125 |
+
|
126 |
+
prefs.end();
|
127 |
+
return readSize == sizeof(float) * LOAD_POINTS * RPM_POINTS;
|
128 |
+
}
|
fuel_learning.h
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef FUEL_LEARNING_H
|
2 |
+
#define FUEL_LEARNING_H
|
3 |
+
|
4 |
+
#include <Arduino.h>
|
5 |
+
#include "config.h"
|
6 |
+
|
7 |
+
// Размеры таблиц обучения
|
8 |
+
#define LOAD_POINTS 16 // Точки по нагрузке
|
9 |
+
#define RPM_POINTS 16 // Точки по оборотам
|
10 |
+
|
11 |
+
class FuelLearningTable {
|
12 |
+
private:
|
13 |
+
float cells[LOAD_POINTS][RPM_POINTS]; // Таблица коррекции
|
14 |
+
float loadPoints[LOAD_POINTS]; // Точки по нагрузке (кПа)
|
15 |
+
float rpmPoints[RPM_POINTS]; // Точки по оборотам
|
16 |
+
float learningRate; // Скорость обучения
|
17 |
+
float maxCorrection; // Максимальная коррекция
|
18 |
+
float minCorrection; // Минимальная коррекция
|
19 |
+
|
20 |
+
// Вспомогательные методы
|
21 |
+
int findNearestIndex(float value, const float* points, int size);
|
22 |
+
float interpolate2D(float load, float rpm);
|
23 |
+
void updateCell(int loadIdx, int rpmIdx, float correction);
|
24 |
+
|
25 |
+
public:
|
26 |
+
FuelLearningTable();
|
27 |
+
|
28 |
+
// Основные методы
|
29 |
+
void begin();
|
30 |
+
float getCorrection(float load, float rpm);
|
31 |
+
void learn(float load, float rpm, float lambdaError);
|
32 |
+
void reset();
|
33 |
+
|
34 |
+
// Методы сохранения/загрузки
|
35 |
+
bool save();
|
36 |
+
bool load();
|
37 |
+
|
38 |
+
// Настройка параметров
|
39 |
+
void setLearningRate(float rate);
|
40 |
+
void setCorrectionLimits(float min, float max);
|
41 |
+
|
42 |
+
// Доступ к данным для визуализации
|
43 |
+
float getCellValue(int loadIdx, int rpmIdx);
|
44 |
+
float getLoadPoint(int idx);
|
45 |
+
float getRPMPoint(int idx);
|
46 |
+
};
|
47 |
+
|
48 |
+
#endif
|
fuel_learning_table.cpp
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "fuel_learning_table.h"
|
2 |
+
#include "SD.h"
|
3 |
+
#include "maps.h"
|
4 |
+
|
5 |
+
FuelLearningTable::FuelLearningTable() {
|
6 |
+
reset();
|
7 |
+
}
|
8 |
+
|
9 |
+
void FuelLearningTable::reset() {
|
10 |
+
for (int i = 0; i < 16; i++) {
|
11 |
+
for (int j = 0; j < 16; j++) {
|
12 |
+
corrections[i][j] = 1.0;
|
13 |
+
cellHits[i][j] = 0;
|
14 |
+
}
|
15 |
+
}
|
16 |
+
}
|
17 |
+
|
18 |
+
bool FuelLearningTable::save() {
|
19 |
+
File file = SD.open("/fuel_learning.bin", FILE_WRITE);
|
20 |
+
if (!file) return false;
|
21 |
+
|
22 |
+
file.write((uint8_t*)corrections, sizeof(corrections));
|
23 |
+
file.write((uint8_t*)cellHits, sizeof(cellHits));
|
24 |
+
file.close();
|
25 |
+
return true;
|
26 |
+
}
|
27 |
+
|
28 |
+
bool FuelLearningTable::load() {
|
29 |
+
File file = SD.open("/fuel_learning.bin", FILE_READ);
|
30 |
+
if (!file) return false;
|
31 |
+
|
32 |
+
file.read((uint8_t*)corrections, sizeof(corrections));
|
33 |
+
file.read((uint8_t*)cellHits, sizeof(cellHits));
|
34 |
+
file.close();
|
35 |
+
return true;
|
36 |
+
}
|
37 |
+
|
38 |
+
float FuelLearningTable::getCorrection(float load, float rpm) {
|
39 |
+
int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
|
40 |
+
int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
|
41 |
+
return corrections[loadIdx][rpmIdx];
|
42 |
+
}
|
43 |
+
|
44 |
+
void FuelLearningTable::update(float load, float rpm, float correction) {
|
45 |
+
int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
|
46 |
+
int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
|
47 |
+
|
48 |
+
float currentCorr = corrections[loadIdx][rpmIdx];
|
49 |
+
float learningRate = 0.1f / (1.0f + cellHits[loadIdx][rpmIdx]);
|
50 |
+
corrections[loadIdx][rpmIdx] = currentCorr + (correction - currentCorr) * learningRate;
|
51 |
+
cellHits[loadIdx][rpmIdx]++;
|
52 |
+
}
|
53 |
+
|
54 |
+
float FuelLearningTable::getProgress() const {
|
55 |
+
int totalCells = 16 * 16;
|
56 |
+
int learnedCells = 0;
|
57 |
+
|
58 |
+
for (int i = 0; i < 16; i++) {
|
59 |
+
for (int j = 0; j < 16; j++) {
|
60 |
+
if (cellHits[i][j] > 0) learnedCells++;
|
61 |
+
}
|
62 |
+
}
|
63 |
+
|
64 |
+
return (float)learnedCells / totalCells;
|
65 |
+
}
|
fuel_learning_table.h
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
#include "Arduino.h"
|
3 |
+
|
4 |
+
class FuelLearningTable {
|
5 |
+
private:
|
6 |
+
float corrections[16][16];
|
7 |
+
uint32_t cellHits[16][16];
|
8 |
+
|
9 |
+
public:
|
10 |
+
FuelLearningTable();
|
11 |
+
void reset();
|
12 |
+
bool save();
|
13 |
+
bool load();
|
14 |
+
float getCorrection(float load, float rpm);
|
15 |
+
void update(float load, float rpm, float correction);
|
16 |
+
float getProgress() const;
|
17 |
+
};
|
idle_control.cpp
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "idle_control.h"
|
2 |
+
#include "config.h"
|
3 |
+
#include <Arduino.h>
|
4 |
+
#include "maps.h"
|
5 |
+
|
6 |
+
IdleController::IdleController() :
|
7 |
+
targetRPM(800),
|
8 |
+
currentPosition(0),
|
9 |
+
lastUpdateTime(0),
|
10 |
+
isAirConditionerOn(false),
|
11 |
+
engineTemp(20.0),
|
12 |
+
engineControl(nullptr),
|
13 |
+
pid(0.1, 0.05, 0.02) // Начальные значения коэффициентов ПИД
|
14 |
+
{
|
15 |
+
}
|
16 |
+
|
17 |
+
void IdleController::begin() {
|
18 |
+
// Настройка ШИМ каналов для РХХ
|
19 |
+
ledcSetup(0, IDLE_VALVE_PWM_FREQ, IDLE_VALVE_PWM_RES); // Канал 0 для пина A
|
20 |
+
ledcSetup(1, IDLE_VALVE_PWM_FREQ, IDLE_VALVE_PWM_RES); // Канал 1 для пина B
|
21 |
+
|
22 |
+
ledcAttachPin(IDLE_VALVE_PIN_A, 0); // Привязываем пин A к каналу 0
|
23 |
+
ledcAttachPin(IDLE_VALVE_PIN_B, 1); // Привязываем пин B к каналу 1
|
24 |
+
|
25 |
+
// Начальные значения ШИМ
|
26 |
+
ledcWrite(0, 0);
|
27 |
+
ledcWrite(1, 0);
|
28 |
+
|
29 |
+
// Инициализация ПИД-регулятора
|
30 |
+
pid.SetMode(true); // Включаем автоматический режим
|
31 |
+
pid.SetOutputLimits(0, 255);
|
32 |
+
pid.SetSampleTime(50);
|
33 |
+
|
34 |
+
lastUpdateTime = millis();
|
35 |
+
|
36 |
+
// Установка начального положения
|
37 |
+
updateValvePosition(0);
|
38 |
+
}
|
39 |
+
|
40 |
+
void IdleController::updateValvePosition(float position) {
|
41 |
+
position = constrain(position, 0, 255);
|
42 |
+
currentPosition = position;
|
43 |
+
|
44 |
+
// Преобразование позиции в ШИМ
|
45 |
+
int pwmValue = mapf(position, 0, 255, 0, (1 << IDLE_VALVE_PWM_RES) - 1);
|
46 |
+
|
47 |
+
// Управление моторчиком РХХ
|
48 |
+
ledcWrite(0, pwmValue);
|
49 |
+
ledcWrite(1, 0);
|
50 |
+
}
|
51 |
+
|
52 |
+
void IdleController::update(float currentRPM) {
|
53 |
+
unsigned long currentTime = millis();
|
54 |
+
float dt = (currentTime - lastUpdateTime) / 1000.0;
|
55 |
+
lastUpdateTime = currentTime;
|
56 |
+
|
57 |
+
// Проверяем наличие объекта управления двигателем
|
58 |
+
if (!engineControl) return;
|
59 |
+
|
60 |
+
// Получаем текущую температуру двигателя
|
61 |
+
float engineTemp = engineControl->getEngineTemp();
|
62 |
+
|
63 |
+
// Корректируем целевые обороты в зависимости от температуры
|
64 |
+
float adjustedTargetRpm = targetRPM;
|
65 |
+
if (engineTemp < 60) { // Если двигатель холодный
|
66 |
+
adjustedTargetRpm += (60 - engineTemp) * 6.67f; // Увеличиваем на 400 RPM при 0°C, линейно уменьшая до 60°C
|
67 |
+
}
|
68 |
+
|
69 |
+
// Добавляем компенсацию для кондиционера
|
70 |
+
if (isAirConditionerOn) {
|
71 |
+
adjustedTargetRpm += 50; // Увеличиваем на 50 RPM при включенном кондиционере
|
72 |
+
}
|
73 |
+
|
74 |
+
// Обновляем ПИД
|
75 |
+
pid.SetSetpoint(adjustedTargetRpm);
|
76 |
+
pid.SetInput(currentRPM);
|
77 |
+
if (pid.Compute()) {
|
78 |
+
// Применяем выход ПИД к положению РХХ
|
79 |
+
setPosition(pid.GetOutput());
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
void IdleController::setEngineTemperature(float temp) {
|
84 |
+
engineTemp = temp;
|
85 |
+
}
|
86 |
+
|
87 |
+
void IdleController::setAirConditioner(bool isOn) {
|
88 |
+
isAirConditionerOn = isOn;
|
89 |
+
}
|
90 |
+
|
91 |
+
void IdleController::loadSettings() {
|
92 |
+
// Здесь должна быть загрузка настроек из EEPROM или SD
|
93 |
+
// Пока используем значения по умолчанию
|
94 |
+
settings.targetRPM = 800;
|
95 |
+
settings.valveMinPosition = 0;
|
96 |
+
settings.valveMaxPosition = 255;
|
97 |
+
settings.rpmTolerance = 50;
|
98 |
+
}
|
99 |
+
|
100 |
+
void IdleController::saveSettings() {
|
101 |
+
// Здесь должно быть сохранение настроек в EEPROM или SD
|
102 |
+
}
|
idle_control.h
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
#include "Arduino.h"
|
3 |
+
#include "config.h"
|
4 |
+
#include "engine_control.h"
|
5 |
+
#include "pid_controller.h"
|
6 |
+
|
7 |
+
class IdleController {
|
8 |
+
private:
|
9 |
+
float targetRPM;
|
10 |
+
float currentPosition;
|
11 |
+
unsigned long lastUpdateTime;
|
12 |
+
bool isAirConditionerOn;
|
13 |
+
float engineTemp;
|
14 |
+
|
15 |
+
// ПИД-регулятор
|
16 |
+
PIDController pid;
|
17 |
+
|
18 |
+
// Указатель на объект управления двигателем для получения температуры
|
19 |
+
EngineControl* engineControl;
|
20 |
+
|
21 |
+
// Вспомогательные методы
|
22 |
+
void updateValvePosition(float position);
|
23 |
+
|
24 |
+
public:
|
25 |
+
IdleController();
|
26 |
+
void begin();
|
27 |
+
void update(float currentRPM);
|
28 |
+
|
29 |
+
// Методы для работы с целевыми оборотами
|
30 |
+
void setTargetRPM(float rpm) {
|
31 |
+
if (rpm >= 700 && rpm <= 2000) {
|
32 |
+
targetRPM = rpm;
|
33 |
+
pid.SetSetpoint(rpm);
|
34 |
+
}
|
35 |
+
}
|
36 |
+
float getTargetRPM() const { return targetRPM; }
|
37 |
+
|
38 |
+
// Методы для настройки ПИД
|
39 |
+
void setPIDParameters(float kp, float ki, float kd) {
|
40 |
+
pid.SetTunings(kp, ki, kd);
|
41 |
+
}
|
42 |
+
|
43 |
+
// Методы для управления положением
|
44 |
+
void setPosition(float position) { updateValvePosition(position); }
|
45 |
+
float getCurrentPosition() const { return currentPosition; }
|
46 |
+
|
47 |
+
// Методы для работы с температурой и кондиционером
|
48 |
+
void setEngineTemperature(float temp) { engineTemp = temp; }
|
49 |
+
void setAirConditioner(bool isOn) { isAirConditionerOn = isOn; }
|
50 |
+
|
51 |
+
// Метод для установки указателя на объект управления двигателем
|
52 |
+
void setEngineControl(EngineControl* control) { engineControl = control; }
|
53 |
+
};
|
ignition_learning.cpp
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "ignition_learning.h"
|
2 |
+
#include <Preferences.h>
|
3 |
+
|
4 |
+
IgnitionLearningTable::IgnitionLearningTable() :
|
5 |
+
learningRate(0.05),
|
6 |
+
maxCorrection(5.0),
|
7 |
+
minCorrection(-5.0),
|
8 |
+
knockThreshold(0.8),
|
9 |
+
knockProtectionOffset(2.0),
|
10 |
+
knockHistorySize(10)
|
11 |
+
{
|
12 |
+
// Инициализация точек нагрузки (кПа)
|
13 |
+
for (int i = 0; i < LOAD_POINTS; i++) {
|
14 |
+
loadPoints[i] = 20.0 + (i * 6.0); // 20-110 кПа
|
15 |
+
}
|
16 |
+
|
17 |
+
// Инициализация точек оборотов
|
18 |
+
for (int i = 0; i < RPM_POINTS; i++) {
|
19 |
+
rpmPoints[i] = 800.0 + (i * 400.0); // 800-6800 RPM
|
20 |
+
}
|
21 |
+
|
22 |
+
// Инициализация ячеек коррекции и истории детонации
|
23 |
+
reset();
|
24 |
+
}
|
25 |
+
|
26 |
+
void IgnitionLearningTable::begin() {
|
27 |
+
load(); // Загрузка сохраненных данных
|
28 |
+
}
|
29 |
+
|
30 |
+
void IgnitionLearningTable::reset() {
|
31 |
+
for (int i = 0; i < LOAD_POINTS; i++) {
|
32 |
+
for (int j = 0; j < RPM_POINTS; j++) {
|
33 |
+
cells[i][j] = 0.0; // Нейтральная коррекция
|
34 |
+
knockHistory[i][j] = 0; // Сброс истории детонации
|
35 |
+
}
|
36 |
+
}
|
37 |
+
}
|
38 |
+
|
39 |
+
int IgnitionLearningTable::findNearestIndex(float value, const float* points, int size) {
|
40 |
+
if (value <= points[0]) return 0;
|
41 |
+
if (value >= points[size-1]) return size - 2;
|
42 |
+
|
43 |
+
for (int i = 0; i < size - 1; i++) {
|
44 |
+
if (value >= points[i] && value < points[i+1]) {
|
45 |
+
return i;
|
46 |
+
}
|
47 |
+
}
|
48 |
+
return 0;
|
49 |
+
}
|
50 |
+
|
51 |
+
float IgnitionLearningTable::interpolate2D(float load, float rpm) {
|
52 |
+
// Находим ближайшие индексы
|
53 |
+
int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
|
54 |
+
int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
|
55 |
+
|
56 |
+
// Находим веса для интерполяции
|
57 |
+
float loadWeight = (load - loadPoints[loadIdx]) /
|
58 |
+
(loadPoints[loadIdx+1] - loadPoints[loadIdx]);
|
59 |
+
float rpmWeight = (rpm - rpmPoints[rpmIdx]) /
|
60 |
+
(rpmPoints[rpmIdx+1] - rpmPoints[rpmIdx]);
|
61 |
+
|
62 |
+
// Билинейная интерполяция
|
63 |
+
float c00 = cells[loadIdx][rpmIdx];
|
64 |
+
float c10 = cells[loadIdx+1][rpmIdx];
|
65 |
+
float c01 = cells[loadIdx][rpmIdx+1];
|
66 |
+
float c11 = cells[loadIdx+1][rpmIdx+1];
|
67 |
+
|
68 |
+
float c0 = c00 * (1 - loadWeight) + c10 * loadWeight;
|
69 |
+
float c1 = c01 * (1 - loadWeight) + c11 * loadWeight;
|
70 |
+
|
71 |
+
return c0 * (1 - rpmWeight) + c1 * rpmWeight;
|
72 |
+
}
|
73 |
+
|
74 |
+
void IgnitionLearningTable::updateCell(int loadIdx, int rpmIdx, float correction) {
|
75 |
+
// Применяем коррекцию с учетом скорости обучения
|
76 |
+
cells[loadIdx][rpmIdx] += correction * learningRate;
|
77 |
+
|
78 |
+
// Ограничиваем значение коррекции
|
79 |
+
cells[loadIdx][rpmIdx] = constrain(cells[loadIdx][rpmIdx],
|
80 |
+
minCorrection, maxCorrection);
|
81 |
+
}
|
82 |
+
|
83 |
+
void IgnitionLearningTable::updateKnockHistory(int loadIdx, int rpmIdx, bool knockDetected) {
|
84 |
+
// Сдвигаем историю
|
85 |
+
knockHistory[loadIdx][rpmIdx] = (knockHistory[loadIdx][rpmIdx] << 1) | (knockDetected ? 1 : 0);
|
86 |
+
knockHistory[loadIdx][rpmIdx] &= ((1 << knockHistorySize) - 1); // Ограничиваем размер
|
87 |
+
}
|
88 |
+
|
89 |
+
float IgnitionLearningTable::getCorrection(float load, float rpm) {
|
90 |
+
return interpolate2D(load, rpm);
|
91 |
+
}
|
92 |
+
|
93 |
+
void IgnitionLearningTable::learn(float load, float rpm, float knockLevel) {
|
94 |
+
// Находим ближайшие ячейки
|
95 |
+
int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
|
96 |
+
int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
|
97 |
+
|
98 |
+
bool knockDetected = knockLevel > knockThreshold;
|
99 |
+
updateKnockHistory(loadIdx, rpmIdx, knockDetected);
|
100 |
+
|
101 |
+
float correction = 0;
|
102 |
+
|
103 |
+
if (knockDetected) {
|
104 |
+
// При детонации уменьшаем УОЗ
|
105 |
+
correction = -knockProtectionOffset;
|
106 |
+
} else {
|
107 |
+
// Если детонации нет, медленно увеличиваем УОЗ
|
108 |
+
// но только если в истории нет детонации
|
109 |
+
if (knockHistory[loadIdx][rpmIdx] == 0) {
|
110 |
+
correction = 0.1;
|
111 |
+
}
|
112 |
+
}
|
113 |
+
|
114 |
+
// Обновляем основную ячейку
|
115 |
+
updateCell(loadIdx, rpmIdx, correction);
|
116 |
+
|
117 |
+
// Обновляем соседние ячейки с меньшим весом
|
118 |
+
if (knockDetected) {
|
119 |
+
if (loadIdx > 0) {
|
120 |
+
updateCell(loadIdx-1, rpmIdx, correction * 0.5);
|
121 |
+
}
|
122 |
+
if (loadIdx < LOAD_POINTS-1) {
|
123 |
+
updateCell(loadIdx+1, rpmIdx, correction * 0.5);
|
124 |
+
}
|
125 |
+
if (rpmIdx > 0) {
|
126 |
+
updateCell(loadIdx, rpmIdx-1, correction * 0.5);
|
127 |
+
}
|
128 |
+
if (rpmIdx < RPM_POINTS-1) {
|
129 |
+
updateCell(loadIdx, rpmIdx+1, correction * 0.5);
|
130 |
+
}
|
131 |
+
}
|
132 |
+
}
|
133 |
+
|
134 |
+
bool IgnitionLearningTable::save() {
|
135 |
+
Preferences prefs;
|
136 |
+
prefs.begin("ign_learn", false);
|
137 |
+
|
138 |
+
// Сохраняем таблицу коррекции
|
139 |
+
size_t written = prefs.putBytes("cells", cells,
|
140 |
+
sizeof(float) * LOAD_POINTS * RPM_POINTS);
|
141 |
+
|
142 |
+
// Сохраняем историю детонации
|
143 |
+
written += prefs.putBytes("knock_hist", knockHistory,
|
144 |
+
sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS);
|
145 |
+
|
146 |
+
prefs.end();
|
147 |
+
return written == sizeof(float) * LOAD_POINTS * RPM_POINTS +
|
148 |
+
sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS;
|
149 |
+
}
|
150 |
+
|
151 |
+
bool IgnitionLearningTable::load() {
|
152 |
+
Preferences prefs;
|
153 |
+
prefs.begin("ign_learn", true);
|
154 |
+
|
155 |
+
// Загружаем таблицу коррекции
|
156 |
+
size_t readSize = prefs.getBytes("cells", cells,
|
157 |
+
sizeof(float) * LOAD_POINTS * RPM_POINTS);
|
158 |
+
|
159 |
+
// Загружаем историю детонации
|
160 |
+
readSize += prefs.getBytes("knock_hist", knockHistory,
|
161 |
+
sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS);
|
162 |
+
|
163 |
+
prefs.end();
|
164 |
+
return readSize == sizeof(float) * LOAD_POINTS * RPM_POINTS +
|
165 |
+
sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS;
|
166 |
+
}
|
ignition_learning.h
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef IGNITION_LEARNING_H
|
2 |
+
#define IGNITION_LEARNING_H
|
3 |
+
|
4 |
+
#include <Arduino.h>
|
5 |
+
#include "config.h"
|
6 |
+
|
7 |
+
// Размеры таблиц обучения
|
8 |
+
#define LOAD_POINTS 16 // Точки по нагрузке
|
9 |
+
#define RPM_POINTS 16 // Точки по оборотам
|
10 |
+
|
11 |
+
class IgnitionLearningTable {
|
12 |
+
private:
|
13 |
+
float cells[LOAD_POINTS][RPM_POINTS]; // Таблица коррекции УОЗ
|
14 |
+
float loadPoints[LOAD_POINTS]; // Точки по нагрузке (кПа)
|
15 |
+
float rpmPoints[RPM_POINTS]; // Точки по оборотам
|
16 |
+
float learningRate; // Скорость обучения
|
17 |
+
float maxCorrection; // Максимальная коррекция
|
18 |
+
float minCorrection; // Минимальная коррекция
|
19 |
+
float knockThreshold; // Порог детонации
|
20 |
+
float knockProtectionOffset; // Защитное смещение при детонации
|
21 |
+
|
22 |
+
// История детонации для каждой ячейки
|
23 |
+
uint8_t knockHistory[LOAD_POINTS][RPM_POINTS];
|
24 |
+
uint8_t knockHistorySize;
|
25 |
+
|
26 |
+
// Вспомогательные методы
|
27 |
+
int findNearestIndex(float value, const float* points, int size);
|
28 |
+
float interpolate2D(float load, float rpm);
|
29 |
+
void updateCell(int loadIdx, int rpmIdx, float correction);
|
30 |
+
void updateKnockHistory(int loadIdx, int rpmIdx, bool knockDetected);
|
31 |
+
|
32 |
+
public:
|
33 |
+
IgnitionLearningTable();
|
34 |
+
|
35 |
+
// Основные методы
|
36 |
+
void begin();
|
37 |
+
float getCorrection(float load, float rpm);
|
38 |
+
void learn(float load, float rpm, float knockLevel);
|
39 |
+
void reset();
|
40 |
+
|
41 |
+
// Методы сохранения/загрузки
|
42 |
+
bool save();
|
43 |
+
bool load();
|
44 |
+
|
45 |
+
// Настройка параметров
|
46 |
+
void setLearningRate(float rate);
|
47 |
+
void setCorrectionLimits(float min, float max);
|
48 |
+
void setKnockParameters(float threshold, float protection);
|
49 |
+
|
50 |
+
// Доступ к данным для визуализации
|
51 |
+
float getCellValue(int loadIdx, int rpmIdx);
|
52 |
+
float getLoadPoint(int idx);
|
53 |
+
float getRPMPoint(int idx);
|
54 |
+
uint8_t getKnockCount(int loadIdx, int rpmIdx);
|
55 |
+
};
|
56 |
+
|
57 |
+
#endif
|
ignition_learning_table.cpp
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "ignition_learning_table.h"
|
2 |
+
#include "SD.h"
|
3 |
+
#include "maps.h"
|
4 |
+
|
5 |
+
IgnitionLearningTable::IgnitionLearningTable() {
|
6 |
+
reset();
|
7 |
+
}
|
8 |
+
|
9 |
+
void IgnitionLearningTable::reset() {
|
10 |
+
for (int i = 0; i < 16; i++) {
|
11 |
+
for (int j = 0; j < 16; j++) {
|
12 |
+
corrections[i][j] = 0.0;
|
13 |
+
cellHits[i][j] = 0;
|
14 |
+
}
|
15 |
+
}
|
16 |
+
}
|
17 |
+
|
18 |
+
bool IgnitionLearningTable::save() {
|
19 |
+
File file = SD.open("/ignition_learning.bin", FILE_WRITE);
|
20 |
+
if (!file) return false;
|
21 |
+
|
22 |
+
file.write((uint8_t*)corrections, sizeof(corrections));
|
23 |
+
file.write((uint8_t*)cellHits, sizeof(cellHits));
|
24 |
+
file.close();
|
25 |
+
return true;
|
26 |
+
}
|
27 |
+
|
28 |
+
bool IgnitionLearningTable::load() {
|
29 |
+
File file = SD.open("/ignition_learning.bin", FILE_READ);
|
30 |
+
if (!file) return false;
|
31 |
+
|
32 |
+
file.read((uint8_t*)corrections, sizeof(corrections));
|
33 |
+
file.read((uint8_t*)cellHits, sizeof(cellHits));
|
34 |
+
file.close();
|
35 |
+
return true;
|
36 |
+
}
|
37 |
+
|
38 |
+
float IgnitionLearningTable::getCorrection(float load, float rpm) {
|
39 |
+
int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
|
40 |
+
int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
|
41 |
+
return corrections[loadIdx][rpmIdx];
|
42 |
+
}
|
43 |
+
|
44 |
+
void IgnitionLearningTable::learn(float load, float rpm, float knockLevel) {
|
45 |
+
int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
|
46 |
+
int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
|
47 |
+
|
48 |
+
if (knockLevel > 0.5) { // Если есть детонация
|
49 |
+
float currentCorr = corrections[loadIdx][rpmIdx];
|
50 |
+
float learningRate = 0.5f / (1.0f + cellHits[loadIdx][rpmIdx]);
|
51 |
+
corrections[loadIdx][rpmIdx] = currentCorr - learningRate; // Уменьшаем УОЗ
|
52 |
+
}
|
53 |
+
|
54 |
+
cellHits[loadIdx][rpmIdx]++;
|
55 |
+
}
|
56 |
+
|
57 |
+
float IgnitionLearningTable::getProgress() const {
|
58 |
+
int totalCells = 16 * 16;
|
59 |
+
int learnedCells = 0;
|
60 |
+
|
61 |
+
for (int i = 0; i < 16; i++) {
|
62 |
+
for (int j = 0; j < 16; j++) {
|
63 |
+
if (cellHits[i][j] > 0) learnedCells++;
|
64 |
+
}
|
65 |
+
}
|
66 |
+
|
67 |
+
return (float)learnedCells / totalCells;
|
68 |
+
}
|
ignition_learning_table.h
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
#include "Arduino.h"
|
3 |
+
|
4 |
+
class IgnitionLearningTable {
|
5 |
+
private:
|
6 |
+
float corrections[16][16];
|
7 |
+
uint32_t cellHits[16][16];
|
8 |
+
|
9 |
+
public:
|
10 |
+
IgnitionLearningTable();
|
11 |
+
void reset();
|
12 |
+
bool save();
|
13 |
+
bool load();
|
14 |
+
float getCorrection(float load, float rpm);
|
15 |
+
void learn(float load, float rpm, float knockLevel);
|
16 |
+
float getProgress() const;
|
17 |
+
};
|
learning_manager.cpp
ADDED
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "learning_manager.h"
|
2 |
+
#include <ArduinoJson.h>
|
3 |
+
|
4 |
+
LearningManager::LearningManager() :
|
5 |
+
isLearningEnabled(false),
|
6 |
+
isFuelLearningEnabled(true),
|
7 |
+
isIgnitionLearningEnabled(true),
|
8 |
+
learningStartTime(0),
|
9 |
+
minLearningTime(300000), // 5 минут
|
10 |
+
minEngineTemp(60), // 60°C
|
11 |
+
maxEngineTemp(100), // 100°C
|
12 |
+
minThrottlePosition(5), // 5%
|
13 |
+
maxThrottlePosition(95), // 95%
|
14 |
+
totalLearningPoints(0),
|
15 |
+
knockEvents(0),
|
16 |
+
richMixtureEvents(0),
|
17 |
+
leanMixtureEvents(0)
|
18 |
+
{
|
19 |
+
}
|
20 |
+
|
21 |
+
void LearningManager::begin() {
|
22 |
+
fuelTable.begin();
|
23 |
+
ignitionTable.begin();
|
24 |
+
|
25 |
+
// Загружаем сохраненные данные
|
26 |
+
load();
|
27 |
+
|
28 |
+
// Устанавливаем параметры обучения
|
29 |
+
fuelTable.setLearningRate(0.01);
|
30 |
+
fuelTable.setCorrectionLimits(0.8, 1.2);
|
31 |
+
|
32 |
+
ignitionTable.setLearningRate(0.05);
|
33 |
+
ignitionTable.setCorrectionLimits(-5.0, 5.0);
|
34 |
+
ignitionTable.setKnockParameters(0.8, 2.0);
|
35 |
+
|
36 |
+
learningStartTime = millis();
|
37 |
+
}
|
38 |
+
|
39 |
+
bool LearningManager::checkLearningConditions(float engineTemp, float throttlePosition) {
|
40 |
+
// Проверка времени работы двигателя
|
41 |
+
if (millis() - learningStartTime < minLearningTime) {
|
42 |
+
return false;
|
43 |
+
}
|
44 |
+
|
45 |
+
// Проверка температуры двигателя
|
46 |
+
if (engineTemp < minEngineTemp || engineTemp > maxEngineTemp) {
|
47 |
+
return false;
|
48 |
+
}
|
49 |
+
|
50 |
+
// Проверка положения дросселя
|
51 |
+
if (throttlePosition < minThrottlePosition || throttlePosition > maxThrottlePosition) {
|
52 |
+
return false;
|
53 |
+
}
|
54 |
+
|
55 |
+
return true;
|
56 |
+
}
|
57 |
+
|
58 |
+
void LearningManager::update(float load, float rpm, float lambda, float knockLevel,
|
59 |
+
float engineTemp, float throttlePosition) {
|
60 |
+
if (!isLearningEnabled || !checkLearningConditions(engineTemp, throttlePosition)) {
|
61 |
+
return;
|
62 |
+
}
|
63 |
+
|
64 |
+
// Обучение топливной карты
|
65 |
+
if (isFuelLearningEnabled) {
|
66 |
+
float lambdaError = lambda - 1.0; // Отклонение от стехиометрии
|
67 |
+
if (abs(lambdaError) > 0.02) { // Порог ошибки 2%
|
68 |
+
fuelTable.learn(load, rpm, lambdaError);
|
69 |
+
if (lambdaError > 0) {
|
70 |
+
richMixtureEvents++;
|
71 |
+
} else {
|
72 |
+
leanMixtureEvents++;
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
|
77 |
+
// Обучение карты УОЗ
|
78 |
+
if (isIgnitionLearningEnabled) {
|
79 |
+
ignitionTable.learn(load, rpm, knockLevel);
|
80 |
+
if (knockLevel > 0.8) { // Порог детонации
|
81 |
+
knockEvents++;
|
82 |
+
}
|
83 |
+
}
|
84 |
+
|
85 |
+
totalLearningPoints++;
|
86 |
+
}
|
87 |
+
|
88 |
+
float LearningManager::getFuelCorrection(float load, float rpm) {
|
89 |
+
return fuelTable.getCorrection(load, rpm);
|
90 |
+
}
|
91 |
+
|
92 |
+
float LearningManager::getIgnitionCorrection(float load, float rpm) {
|
93 |
+
return ignitionTable.getCorrection(load, rpm);
|
94 |
+
}
|
95 |
+
|
96 |
+
void LearningManager::setLearningParameters(float minTemp, float maxTemp,
|
97 |
+
float minThrottle, float maxThrottle) {
|
98 |
+
minEngineTemp = minTemp;
|
99 |
+
maxEngineTemp = maxTemp;
|
100 |
+
minThrottlePosition = minThrottle;
|
101 |
+
maxThrottlePosition = maxThrottle;
|
102 |
+
}
|
103 |
+
|
104 |
+
void LearningManager::setLearningRates(float fuelRate, float ignitionRate) {
|
105 |
+
fuelTable.setLearningRate(fuelRate);
|
106 |
+
ignitionTable.setLearningRate(ignitionRate);
|
107 |
+
}
|
108 |
+
|
109 |
+
bool LearningManager::save() {
|
110 |
+
bool fuelSaved = fuelTable.save();
|
111 |
+
bool ignitionSaved = ignitionTable.save();
|
112 |
+
|
113 |
+
// Сохраняем статистику
|
114 |
+
Preferences prefs;
|
115 |
+
prefs.begin("learning", false);
|
116 |
+
prefs.putUInt("total_points", totalLearningPoints);
|
117 |
+
prefs.putUInt("knock_events", knockEvents);
|
118 |
+
prefs.putUInt("rich_events", richMixtureEvents);
|
119 |
+
prefs.putUInt("lean_events", leanMixtureEvents);
|
120 |
+
prefs.end();
|
121 |
+
|
122 |
+
return fuelSaved && ignitionSaved;
|
123 |
+
}
|
124 |
+
|
125 |
+
bool LearningManager::load() {
|
126 |
+
bool fuelLoaded = fuelTable.load();
|
127 |
+
bool ignitionLoaded = ignitionTable.load();
|
128 |
+
|
129 |
+
// Загружаем статистику
|
130 |
+
Preferences prefs;
|
131 |
+
prefs.begin("learning", true);
|
132 |
+
totalLearningPoints = prefs.getUInt("total_points", 0);
|
133 |
+
knockEvents = prefs.getUInt("knock_events", 0);
|
134 |
+
richMixtureEvents = prefs.getUInt("rich_events", 0);
|
135 |
+
leanMixtureEvents = prefs.getUInt("lean_events", 0);
|
136 |
+
prefs.end();
|
137 |
+
|
138 |
+
return fuelLoaded && ignitionLoaded;
|
139 |
+
}
|
140 |
+
|
141 |
+
void LearningManager::reset() {
|
142 |
+
fuelTable.reset();
|
143 |
+
ignitionTable.reset();
|
144 |
+
totalLearningPoints = 0;
|
145 |
+
knockEvents = 0;
|
146 |
+
richMixtureEvents = 0;
|
147 |
+
leanMixtureEvents = 0;
|
148 |
+
save();
|
149 |
+
}
|
150 |
+
|
151 |
+
void LearningManager::enable() {
|
152 |
+
isLearningEnabled = true;
|
153 |
+
learningStartTime = millis();
|
154 |
+
}
|
155 |
+
|
156 |
+
void LearningManager::disable() {
|
157 |
+
isLearningEnabled = false;
|
158 |
+
}
|
159 |
+
|
160 |
+
uint32_t LearningManager::getTotalLearningPoints() const {
|
161 |
+
return totalLearningPoints;
|
162 |
+
}
|
163 |
+
|
164 |
+
uint32_t LearningManager::getKnockEvents() const {
|
165 |
+
return knockEvents;
|
166 |
+
}
|
167 |
+
|
168 |
+
uint32_t LearningManager::getRichMixtureEvents() const {
|
169 |
+
return richMixtureEvents;
|
170 |
+
}
|
171 |
+
|
172 |
+
uint32_t LearningManager::getLeanMixtureEvents() const {
|
173 |
+
return leanMixtureEvents;
|
174 |
+
}
|
175 |
+
|
176 |
+
float LearningManager::getLearningProgress() const {
|
177 |
+
// Максимальное количество точек обучения (16x16 ячеек, минимум 10 точек на ячейку)
|
178 |
+
const uint32_t maxPoints = 16 * 16 * 10;
|
179 |
+
return (float)totalLearningPoints / maxPoints * 100.0f;
|
180 |
+
}
|
181 |
+
|
182 |
+
String LearningManager::exportToJson() {
|
183 |
+
DynamicJsonDocument doc(8192);
|
184 |
+
|
185 |
+
// Основные параметры
|
186 |
+
doc["enabled"] = isLearningEnabled;
|
187 |
+
doc["fuel_enabled"] = isFuelLearningEnabled;
|
188 |
+
doc["ignition_enabled"] = isIgnitionLearningEnabled;
|
189 |
+
|
190 |
+
// Статистика
|
191 |
+
JsonObject stats = doc.createNestedObject("statistics");
|
192 |
+
stats["total_points"] = totalLearningPoints;
|
193 |
+
stats["knock_events"] = knockEvents;
|
194 |
+
stats["rich_events"] = richMixtureEvents;
|
195 |
+
stats["lean_events"] = leanMixtureEvents;
|
196 |
+
stats["progress"] = getLearningProgress();
|
197 |
+
|
198 |
+
// Топливная карта
|
199 |
+
JsonArray fuelMap = doc.createNestedArray("fuel_map");
|
200 |
+
for (int i = 0; i < 16; i++) {
|
201 |
+
JsonArray row = fuelMap.createNestedArray();
|
202 |
+
for (int j = 0; j < 16; j++) {
|
203 |
+
row.add(fuelTable.getCellValue(i, j));
|
204 |
+
}
|
205 |
+
}
|
206 |
+
|
207 |
+
// Карта УОЗ
|
208 |
+
JsonArray ignitionMap = doc.createNestedArray("ignition_map");
|
209 |
+
for (int i = 0; i < 16; i++) {
|
210 |
+
JsonArray row = ignitionMap.createNestedArray();
|
211 |
+
for (int j = 0; j < 16; j++) {
|
212 |
+
row.add(ignitionTable.getCellValue(i, j));
|
213 |
+
}
|
214 |
+
}
|
215 |
+
|
216 |
+
String output;
|
217 |
+
serializeJson(doc, output);
|
218 |
+
return output;
|
219 |
+
}
|
220 |
+
|
221 |
+
bool LearningManager::importFromJson(const String& json) {
|
222 |
+
DynamicJsonDocument doc(8192);
|
223 |
+
DeserializationError error = deserializeJson(doc, json);
|
224 |
+
|
225 |
+
if (error) {
|
226 |
+
return false;
|
227 |
+
}
|
228 |
+
|
229 |
+
// Временно отключаем обучение
|
230 |
+
bool wasEnabled = isLearningEnabled;
|
231 |
+
disable();
|
232 |
+
|
233 |
+
// Сбрасываем текущие данные
|
234 |
+
reset();
|
235 |
+
|
236 |
+
// Загружаем новые данные
|
237 |
+
if (doc.containsKey("enabled")) {
|
238 |
+
isLearningEnabled = doc["enabled"];
|
239 |
+
}
|
240 |
+
|
241 |
+
if (doc.containsKey("statistics")) {
|
242 |
+
JsonObject stats = doc["statistics"];
|
243 |
+
totalLearningPoints = stats["total_points"] | 0;
|
244 |
+
knockEvents = stats["knock_events"] | 0;
|
245 |
+
richMixtureEvents = stats["rich_events"] | 0;
|
246 |
+
leanMixtureEvents = stats["lean_events"] | 0;
|
247 |
+
}
|
248 |
+
|
249 |
+
// Восстанавливаем состояние обучения
|
250 |
+
if (wasEnabled) {
|
251 |
+
enable();
|
252 |
+
}
|
253 |
+
|
254 |
+
// Сохраняем изменения
|
255 |
+
return save();
|
256 |
+
}
|
learning_manager.h
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef LEARNING_MANAGER_H
|
2 |
+
#define LEARNING_MANAGER_H
|
3 |
+
|
4 |
+
#include "fuel_learning.h"
|
5 |
+
#include "ignition_learning.h"
|
6 |
+
#include <Preferences.h>
|
7 |
+
|
8 |
+
class LearningManager {
|
9 |
+
private:
|
10 |
+
FuelLearningTable fuelTable;
|
11 |
+
IgnitionLearningTable ignitionTable;
|
12 |
+
|
13 |
+
// Параметры обучения
|
14 |
+
bool isLearningEnabled;
|
15 |
+
bool isFuelLearningEnabled;
|
16 |
+
bool isIgnitionLearningEnabled;
|
17 |
+
unsigned long learningStartTime;
|
18 |
+
unsigned long minLearningTime; // Минимальное время работы для начала обучения
|
19 |
+
float minEngineTemp; // Минимальная температура для обучения
|
20 |
+
float maxEngineTemp; // Максимальная температура для обучения
|
21 |
+
float minThrottlePosition; // Минимальное положение дросселя для обучения
|
22 |
+
float maxThrottlePosition; // Максимальное положение дросселя для обучения
|
23 |
+
|
24 |
+
// Статистика обучения
|
25 |
+
uint32_t totalLearningPoints;
|
26 |
+
uint32_t knockEvents;
|
27 |
+
uint32_t richMixtureEvents;
|
28 |
+
uint32_t leanMixtureEvents;
|
29 |
+
|
30 |
+
// Проверка условий для обучения
|
31 |
+
bool checkLearningConditions(float engineTemp, float throttlePosition);
|
32 |
+
|
33 |
+
public:
|
34 |
+
LearningManager();
|
35 |
+
|
36 |
+
// Инициализация и управление
|
37 |
+
void begin();
|
38 |
+
void enable();
|
39 |
+
void disable();
|
40 |
+
void reset();
|
41 |
+
|
42 |
+
// Основные методы обучения
|
43 |
+
void update(float load, float rpm, float lambda, float knockLevel,
|
44 |
+
float engineTemp, float throttlePosition);
|
45 |
+
|
46 |
+
// Получение коррекций
|
47 |
+
float getFuelCorrection(float load, float rpm);
|
48 |
+
float getIgnitionCorrection(float load, float rpm);
|
49 |
+
|
50 |
+
// Настройка параметров
|
51 |
+
void setLearningParameters(float minTemp, float maxTemp,
|
52 |
+
float minThrottle, float maxThrottle);
|
53 |
+
void setLearningRates(float fuelRate, float ignitionRate);
|
54 |
+
|
55 |
+
// Сохранение/загрузка
|
56 |
+
bool save();
|
57 |
+
bool load();
|
58 |
+
|
59 |
+
// Статистика и диагностика
|
60 |
+
uint32_t getTotalLearningPoints() const;
|
61 |
+
uint32_t getKnockEvents() const;
|
62 |
+
uint32_t getRichMixtureEvents() const;
|
63 |
+
uint32_t getLeanMixtureEvents() const;
|
64 |
+
float getLearningProgress() const; // 0-100%
|
65 |
+
|
66 |
+
// Экспорт данных
|
67 |
+
String exportToJson();
|
68 |
+
bool importFromJson(const String& json);
|
69 |
+
};
|
70 |
+
|
71 |
+
#endif
|
maps.cpp
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "maps.h"
|
2 |
+
#include <Arduino.h>
|
3 |
+
|
4 |
+
// Базовая карта УОЗ (16x16)
|
5 |
+
const float baseIgnitionMap[16][16] = {
|
6 |
+
{10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 35, 35, 35}, // 0% нагрузки
|
7 |
+
{10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 35, 35, 35},
|
8 |
+
{8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 34, 34},
|
9 |
+
{8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 33, 33, 33},
|
10 |
+
{6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 31, 31, 31},
|
11 |
+
{6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 29, 30, 30, 30},
|
12 |
+
{4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 27, 28, 28, 28},
|
13 |
+
{4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 25, 26, 27, 27, 27},
|
14 |
+
{2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23, 24, 25, 25, 25},
|
15 |
+
{2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 21, 22, 23, 24, 24, 24},
|
16 |
+
{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 19, 20, 21, 22, 22, 22},
|
17 |
+
{0, 2, 4, 6, 8, 10, 12, 14, 15, 16, 17, 18, 19, 20, 20, 20},
|
18 |
+
{0, 2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 18, 18},
|
19 |
+
{0, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16, 16},
|
20 |
+
{0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 14},
|
21 |
+
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 13, 13} // 100% нагрузки
|
22 |
+
};
|
23 |
+
|
24 |
+
// Базовая карта длительности впрыска (16x16)
|
25 |
+
const float baseInjectionMap[16][16] = {
|
26 |
+
{1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0}, // 0% нагрузки
|
27 |
+
{1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2},
|
28 |
+
{1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4},
|
29 |
+
{1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6},
|
30 |
+
{1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8},
|
31 |
+
{2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0},
|
32 |
+
{2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2},
|
33 |
+
{2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4},
|
34 |
+
{2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6},
|
35 |
+
{2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8},
|
36 |
+
{3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0},
|
37 |
+
{3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2},
|
38 |
+
{3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4},
|
39 |
+
{3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6},
|
40 |
+
{3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8},
|
41 |
+
{4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8, 7.0} // 100% нагрузки
|
42 |
+
};
|
43 |
+
|
44 |
+
// Вспомогательные функции для преобразования значений
|
45 |
+
float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
|
46 |
+
if (in_max == in_min) return out_min; // Защита от деления на ноль
|
47 |
+
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
48 |
+
}
|
49 |
+
|
50 |
+
float interpolate(float x, float in_min, float in_max, float out_min, float out_max) {
|
51 |
+
if (in_max == in_min) return out_min; // Защита от деления на ноль
|
52 |
+
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
|
53 |
+
}
|
54 |
+
|
55 |
+
int findNearestIndex(float value, float* array, int size) {
|
56 |
+
int idx = 0;
|
57 |
+
float minDiff = abs(value - array[0]);
|
58 |
+
for (int i = 1; i < size; i++) {
|
59 |
+
float diff = abs(value - array[i]);
|
60 |
+
if (diff < minDiff) {
|
61 |
+
minDiff = diff;
|
62 |
+
idx = i;
|
63 |
+
}
|
64 |
+
}
|
65 |
+
return idx;
|
66 |
+
}
|
67 |
+
|
68 |
+
float getBaseIgnition(float load, float rpm) {
|
69 |
+
// Находим ближайшие индексы для оборотов и нагрузки
|
70 |
+
float rpmMap[] = {0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000};
|
71 |
+
float mapMap[] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
72 |
+
int rpmIdx = findNearestIndex(rpm, rpmMap, 16);
|
73 |
+
int loadIdx = findNearestIndex(load, mapMap, 16);
|
74 |
+
|
75 |
+
// Получаем значение из карты
|
76 |
+
float baseValue = baseIgnitionMap[rpmIdx][loadIdx];
|
77 |
+
|
78 |
+
// Если нужна интерполяция и мы не на краях карты
|
79 |
+
if (rpmIdx < 15 && loadIdx < 15) {
|
80 |
+
float rpmFrac = (rpm - rpmMap[rpmIdx]) / (rpmMap[rpmIdx + 1] - rpmMap[rpmIdx]);
|
81 |
+
float loadFrac = (load - mapMap[loadIdx]) / (mapMap[loadIdx + 1] - mapMap[loadIdx]);
|
82 |
+
|
83 |
+
// Билинейная интерполяция
|
84 |
+
float v1 = interpolate(rpmFrac, 0, 1, baseIgnitionMap[rpmIdx][loadIdx], baseIgnitionMap[rpmIdx + 1][loadIdx]);
|
85 |
+
float v2 = interpolate(rpmFrac, 0, 1, baseIgnitionMap[rpmIdx][loadIdx + 1], baseIgnitionMap[rpmIdx + 1][loadIdx + 1]);
|
86 |
+
baseValue = interpolate(loadFrac, 0, 1, v1, v2);
|
87 |
+
}
|
88 |
+
|
89 |
+
return baseValue;
|
90 |
+
}
|
91 |
+
|
92 |
+
float getBaseInjection(float load, float rpm) {
|
93 |
+
// Находим ближайшие индексы для оборотов и нагрузки
|
94 |
+
float rpmMap[] = {0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000};
|
95 |
+
float mapMap[] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
96 |
+
int rpmIdx = findNearestIndex(rpm, rpmMap, 16);
|
97 |
+
int loadIdx = findNearestIndex(load, mapMap, 16);
|
98 |
+
|
99 |
+
// Получаем значение из карты
|
100 |
+
float baseValue = baseInjectionMap[rpmIdx][loadIdx];
|
101 |
+
|
102 |
+
// Если нужна интерполяция и мы не на краях карты
|
103 |
+
if (rpmIdx < 15 && loadIdx < 15) {
|
104 |
+
float rpmFrac = (rpm - rpmMap[rpmIdx]) / (rpmMap[rpmIdx + 1] - rpmMap[rpmIdx]);
|
105 |
+
float loadFrac = (load - mapMap[loadIdx]) / (mapMap[loadIdx + 1] - mapMap[loadIdx]);
|
106 |
+
|
107 |
+
// Билинейная интерполяция
|
108 |
+
float v1 = interpolate(rpmFrac, 0, 1, baseInjectionMap[rpmIdx][loadIdx], baseInjectionMap[rpmIdx + 1][loadIdx]);
|
109 |
+
float v2 = interpolate(rpmFrac, 0, 1, baseInjectionMap[rpmIdx][loadIdx + 1], baseInjectionMap[rpmIdx + 1][loadIdx + 1]);
|
110 |
+
baseValue = interpolate(loadFrac, 0, 1, v1, v2);
|
111 |
+
}
|
112 |
+
|
113 |
+
return baseValue;
|
114 |
+
}
|
maps.h
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef MAPS_H
|
2 |
+
#define MAPS_H
|
3 |
+
|
4 |
+
// Карта оборотов для УОЗ и впрыска (RPM)
|
5 |
+
const float rpmMap[16] = {
|
6 |
+
500, // 0
|
7 |
+
1000, // 1
|
8 |
+
1500, // 2
|
9 |
+
2000, // 3
|
10 |
+
2500, // 4
|
11 |
+
3000, // 5
|
12 |
+
3500, // 6
|
13 |
+
4000, // 7
|
14 |
+
4500, // 8
|
15 |
+
5000, // 9
|
16 |
+
5500, // 10
|
17 |
+
6000, // 11
|
18 |
+
6500, // 12
|
19 |
+
7000, // 13
|
20 |
+
7500, // 14
|
21 |
+
8000 // 15
|
22 |
+
};
|
23 |
+
|
24 |
+
// Карта давления для УОЗ и впрыска (MAP в барах)
|
25 |
+
const float mapMap[16] = {
|
26 |
+
0.2, // 0
|
27 |
+
0.3, // 1
|
28 |
+
0.4, // 2
|
29 |
+
0.5, // 3
|
30 |
+
0.6, // 4
|
31 |
+
0.7, // 5
|
32 |
+
0.8, // 6
|
33 |
+
0.9, // 7
|
34 |
+
1.0, // 8
|
35 |
+
1.1, // 9
|
36 |
+
1.2, // 10
|
37 |
+
1.3, // 11
|
38 |
+
1.4, // 12
|
39 |
+
1.5, // 13
|
40 |
+
1.6, // 14
|
41 |
+
1.7 // 15
|
42 |
+
};
|
43 |
+
|
44 |
+
// Карта УОЗ (градусы)
|
45 |
+
const float ignitionMap[16][16] = {
|
46 |
+
// MAP -> 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7
|
47 |
+
{5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, // 500 RPM
|
48 |
+
{6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}, // 1000 RPM
|
49 |
+
{7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}, // 1500 RPM
|
50 |
+
{8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, // 2000 RPM
|
51 |
+
{9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}, // 2500 RPM
|
52 |
+
{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}, // 3000 RPM
|
53 |
+
{11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, // 3500 RPM
|
54 |
+
{12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}, // 4000 RPM
|
55 |
+
{13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28}, // 4500 RPM
|
56 |
+
{14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}, // 5000 RPM
|
57 |
+
{15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}, // 5500 RPM
|
58 |
+
{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}, // 6000 RPM
|
59 |
+
{17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, // 6500 RPM
|
60 |
+
{18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}, // 7000 RPM
|
61 |
+
{19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}, // 7500 RPM
|
62 |
+
{20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35} // 8000 RPM
|
63 |
+
};
|
64 |
+
|
65 |
+
// Карта впрыска (мс)
|
66 |
+
const float injectionMap[16][16] = {
|
67 |
+
// MAP -> 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7
|
68 |
+
{1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0}, // 500 RPM
|
69 |
+
{1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2}, // 1000 RPM
|
70 |
+
{1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4}, // 1500 RPM
|
71 |
+
{1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6}, // 2000 RPM
|
72 |
+
{1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8}, // 2500 RPM
|
73 |
+
{2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0}, // 3000 RPM
|
74 |
+
{2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2}, // 3500 RPM
|
75 |
+
{2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4}, // 4000 RPM
|
76 |
+
{2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6}, // 4500 RPM
|
77 |
+
{2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8}, // 5000 RPM
|
78 |
+
{3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0}, // 5500 RPM
|
79 |
+
{3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2}, // 6000 RPM
|
80 |
+
{3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4}, // 6500 RPM
|
81 |
+
{3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6}, // 7000 RPM
|
82 |
+
{3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8}, // 7500 RPM
|
83 |
+
{4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8, 7.0} // 8000 RPM
|
84 |
+
};
|
85 |
+
|
86 |
+
// Функции для работы с картами
|
87 |
+
float mapf(float x, float in_min, float in_max, float out_min, float out_max);
|
88 |
+
float getBaseIgnition(float load, float rpm);
|
89 |
+
float getBaseInjection(float load, float rpm);
|
90 |
+
|
91 |
+
// Вспомогательные функции для интерполяции
|
92 |
+
inline float interpolate(float x, float x1, float x2, float y1, float y2) {
|
93 |
+
return y1 + (x - x1) * (y2 - y1) / (x2 - x1);
|
94 |
+
}
|
95 |
+
|
96 |
+
inline int findNearestIndex(float value, const float* array, int size) {
|
97 |
+
int index = 0;
|
98 |
+
float minDiff = abs(value - array[0]);
|
99 |
+
|
100 |
+
for(int i = 1; i < size; i++) {
|
101 |
+
float diff = abs(value - array[i]);
|
102 |
+
if(diff < minDiff) {
|
103 |
+
minDiff = diff;
|
104 |
+
index = i;
|
105 |
+
}
|
106 |
+
}
|
107 |
+
return index;
|
108 |
+
}
|
109 |
+
|
110 |
+
#endif
|
pid_controller.h
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
#include "Arduino.h"
|
3 |
+
|
4 |
+
class PIDController {
|
5 |
+
private:
|
6 |
+
double kp;
|
7 |
+
double ki;
|
8 |
+
double kd;
|
9 |
+
|
10 |
+
double input;
|
11 |
+
double output;
|
12 |
+
double setpoint;
|
13 |
+
|
14 |
+
unsigned long lastTime;
|
15 |
+
double outputSum;
|
16 |
+
double lastInput;
|
17 |
+
|
18 |
+
double outMin;
|
19 |
+
double outMax;
|
20 |
+
int sampleTime;
|
21 |
+
bool inAuto;
|
22 |
+
|
23 |
+
public:
|
24 |
+
PIDController(double Kp, double Ki, double Kd) :
|
25 |
+
kp(Kp),
|
26 |
+
ki(Ki),
|
27 |
+
kd(Kd),
|
28 |
+
input(0),
|
29 |
+
output(0),
|
30 |
+
setpoint(0),
|
31 |
+
lastTime(0),
|
32 |
+
outputSum(0),
|
33 |
+
lastInput(0),
|
34 |
+
outMin(0),
|
35 |
+
outMax(255),
|
36 |
+
sampleTime(100),
|
37 |
+
inAuto(false)
|
38 |
+
{
|
39 |
+
}
|
40 |
+
|
41 |
+
bool Compute() {
|
42 |
+
if(!inAuto) return false;
|
43 |
+
|
44 |
+
unsigned long now = millis();
|
45 |
+
unsigned long timeChange = (now - lastTime);
|
46 |
+
if(timeChange >= sampleTime) {
|
47 |
+
double error = setpoint - input;
|
48 |
+
|
49 |
+
outputSum += (ki * error);
|
50 |
+
if(outputSum > outMax) outputSum = outMax;
|
51 |
+
else if(outputSum < outMin) outputSum = outMin;
|
52 |
+
|
53 |
+
double dInput = (input - lastInput);
|
54 |
+
|
55 |
+
output = kp * error + outputSum - kd * dInput;
|
56 |
+
|
57 |
+
if(output > outMax) output = outMax;
|
58 |
+
else if(output < outMin) output = outMin;
|
59 |
+
|
60 |
+
lastInput = input;
|
61 |
+
lastTime = now;
|
62 |
+
|
63 |
+
return true;
|
64 |
+
}
|
65 |
+
return false;
|
66 |
+
}
|
67 |
+
|
68 |
+
void SetTunings(double Kp, double Ki, double Kd) {
|
69 |
+
if (Kp < 0 || Ki < 0 || Kd < 0) return;
|
70 |
+
|
71 |
+
double SampleTimeInSec = ((double)sampleTime)/1000;
|
72 |
+
kp = Kp;
|
73 |
+
ki = Ki * SampleTimeInSec;
|
74 |
+
kd = Kd / SampleTimeInSec;
|
75 |
+
}
|
76 |
+
|
77 |
+
void SetSampleTime(int NewSampleTime) {
|
78 |
+
if (NewSampleTime > 0) {
|
79 |
+
double ratio = (double)NewSampleTime / (double)sampleTime;
|
80 |
+
ki *= ratio;
|
81 |
+
kd /= ratio;
|
82 |
+
sampleTime = (unsigned long)NewSampleTime;
|
83 |
+
}
|
84 |
+
}
|
85 |
+
|
86 |
+
void SetOutputLimits(double Min, double Max) {
|
87 |
+
if(Min >= Max) return;
|
88 |
+
outMin = Min;
|
89 |
+
outMax = Max;
|
90 |
+
|
91 |
+
if(inAuto) {
|
92 |
+
if(output > outMax) output = outMax;
|
93 |
+
else if(output < outMin) output = outMin;
|
94 |
+
|
95 |
+
if(outputSum > outMax) outputSum = outMax;
|
96 |
+
else if(outputSum < outMin) outputSum = outMin;
|
97 |
+
}
|
98 |
+
}
|
99 |
+
|
100 |
+
void SetMode(bool Mode) {
|
101 |
+
bool newAuto = Mode;
|
102 |
+
if(newAuto && !inAuto) {
|
103 |
+
Initialize();
|
104 |
+
}
|
105 |
+
inAuto = newAuto;
|
106 |
+
}
|
107 |
+
|
108 |
+
void Initialize() {
|
109 |
+
outputSum = output;
|
110 |
+
lastInput = input;
|
111 |
+
if(outputSum > outMax) outputSum = outMax;
|
112 |
+
else if(outputSum < outMin) outputSum = outMin;
|
113 |
+
}
|
114 |
+
|
115 |
+
// Методы для установки и получения значений
|
116 |
+
void SetInput(double val) { input = val; }
|
117 |
+
void SetSetpoint(double val) { setpoint = val; }
|
118 |
+
double GetOutput() const { return output; }
|
119 |
+
};
|
settings_manager.cpp
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#include "settings_manager.h"
|
2 |
+
|
3 |
+
const char* SettingsManager::NAMESPACE = "ecu";
|
4 |
+
const char* SettingsManager::SETTINGS_KEY = "settings";
|
5 |
+
|
6 |
+
SettingsManager::SettingsManager() : version(1) {
|
7 |
+
setDefaults();
|
8 |
+
}
|
9 |
+
|
10 |
+
void SettingsManager::setDefaults() {
|
11 |
+
settings.version = version;
|
12 |
+
|
13 |
+
// Настройки РХХ
|
14 |
+
settings.idle.targetRPM = 800;
|
15 |
+
settings.idle.valveMinPosition = 0;
|
16 |
+
settings.idle.valveMaxPosition = 255;
|
17 |
+
settings.idle.rpmTolerance = 50;
|
18 |
+
settings.idle.kp = 0.1;
|
19 |
+
settings.idle.ki = 0.05;
|
20 |
+
settings.idle.kd = 0.02;
|
21 |
+
|
22 |
+
// Общие настройки
|
23 |
+
settings.minAdvance = 0;
|
24 |
+
settings.maxAdvance = 40;
|
25 |
+
settings.injectionTimeMin = 1.0;
|
26 |
+
settings.injectionTimeMax = 20.0;
|
27 |
+
settings.idleRpmMin = 700;
|
28 |
+
settings.idleRpmMax = 900;
|
29 |
+
}
|
30 |
+
|
31 |
+
void SettingsManager::begin() {
|
32 |
+
if (!load()) {
|
33 |
+
setDefaults();
|
34 |
+
save();
|
35 |
+
}
|
36 |
+
}
|
37 |
+
|
38 |
+
bool SettingsManager::load() {
|
39 |
+
// Здесь должна быть загрузка из EEPROM или SD
|
40 |
+
// Пока просто используем значения по умолчанию
|
41 |
+
setDefaults();
|
42 |
+
return true;
|
43 |
+
}
|
44 |
+
|
45 |
+
bool SettingsManager::save() {
|
46 |
+
// Здесь должно быть сохранение в EEPROM или SD
|
47 |
+
return true;
|
48 |
+
}
|
49 |
+
|
50 |
+
void SettingsManager::reset() {
|
51 |
+
setDefaults();
|
52 |
+
save();
|
53 |
+
}
|
54 |
+
|
55 |
+
bool SettingsManager::validateSettings() {
|
56 |
+
if (settings.version != version) return false;
|
57 |
+
|
58 |
+
// Проверка настроек РХХ
|
59 |
+
if (settings.idle.targetRPM < 500 || settings.idle.targetRPM > 2000) return false;
|
60 |
+
if (settings.idle.valveMinPosition < 0 || settings.idle.valveMinPosition > 255) return false;
|
61 |
+
if (settings.idle.valveMaxPosition < 0 || settings.idle.valveMaxPosition > 255) return false;
|
62 |
+
if (settings.idle.valveMaxPosition <= settings.idle.valveMinPosition) return false;
|
63 |
+
if (settings.idle.rpmTolerance < 10 || settings.idle.rpmTolerance > 200) return false;
|
64 |
+
if (settings.idle.kp < 0 || settings.idle.ki < 0 || settings.idle.kd < 0) return false;
|
65 |
+
|
66 |
+
// Проверка общих настроек
|
67 |
+
if (settings.minAdvance < -10 || settings.minAdvance > 20) return false;
|
68 |
+
if (settings.maxAdvance < 20 || settings.maxAdvance > 60) return false;
|
69 |
+
if (settings.maxAdvance <= settings.minAdvance) return false;
|
70 |
+
if (settings.injectionTimeMin < 0.5 || settings.injectionTimeMin > 5.0) return false;
|
71 |
+
if (settings.injectionTimeMax < 5.0 || settings.injectionTimeMax > 30.0) return false;
|
72 |
+
if (settings.injectionTimeMax <= settings.injectionTimeMin) return false;
|
73 |
+
if (settings.idleRpmMin < 500 || settings.idleRpmMin > 1000) return false;
|
74 |
+
if (settings.idleRpmMax < 800 || settings.idleRpmMax > 1500) return false;
|
75 |
+
if (settings.idleRpmMax <= settings.idleRpmMin) return false;
|
76 |
+
|
77 |
+
return true;
|
78 |
+
}
|
79 |
+
|
80 |
+
void SettingsManager::setSettings(const SystemSettings& newSettings) {
|
81 |
+
settings = newSettings;
|
82 |
+
if (validateSettings()) {
|
83 |
+
save();
|
84 |
+
} else {
|
85 |
+
setDefaults();
|
86 |
+
}
|
87 |
+
}
|
88 |
+
|
89 |
+
String SettingsManager::exportToJson() {
|
90 |
+
StaticJsonDocument<1024> doc;
|
91 |
+
|
92 |
+
doc["version"] = settings.version;
|
93 |
+
|
94 |
+
JsonObject idle = doc.createNestedObject("idle");
|
95 |
+
idle["targetRPM"] = settings.idle.targetRPM;
|
96 |
+
idle["valveMinPosition"] = settings.idle.valveMinPosition;
|
97 |
+
idle["valveMaxPosition"] = settings.idle.valveMaxPosition;
|
98 |
+
idle["rpmTolerance"] = settings.idle.rpmTolerance;
|
99 |
+
idle["kp"] = settings.idle.kp;
|
100 |
+
idle["ki"] = settings.idle.ki;
|
101 |
+
idle["kd"] = settings.idle.kd;
|
102 |
+
|
103 |
+
doc["minAdvance"] = settings.minAdvance;
|
104 |
+
doc["maxAdvance"] = settings.maxAdvance;
|
105 |
+
doc["injectionTimeMin"] = settings.injectionTimeMin;
|
106 |
+
doc["injectionTimeMax"] = settings.injectionTimeMax;
|
107 |
+
doc["idleRpmMin"] = settings.idleRpmMin;
|
108 |
+
doc["idleRpmMax"] = settings.idleRpmMax;
|
109 |
+
|
110 |
+
String output;
|
111 |
+
serializeJson(doc, output);
|
112 |
+
return output;
|
113 |
+
}
|
114 |
+
|
115 |
+
bool SettingsManager::importFromJson(const String& json) {
|
116 |
+
StaticJsonDocument<1024> doc;
|
117 |
+
DeserializationError error = deserializeJson(doc, json);
|
118 |
+
|
119 |
+
if (error) return false;
|
120 |
+
|
121 |
+
SystemSettings newSettings;
|
122 |
+
|
123 |
+
newSettings.version = doc["version"] | version;
|
124 |
+
|
125 |
+
JsonObject idle = doc["idle"];
|
126 |
+
if (idle) {
|
127 |
+
newSettings.idle.targetRPM = idle["targetRPM"] | 800;
|
128 |
+
newSettings.idle.valveMinPosition = idle["valveMinPosition"] | 0;
|
129 |
+
newSettings.idle.valveMaxPosition = idle["valveMaxPosition"] | 255;
|
130 |
+
newSettings.idle.rpmTolerance = idle["rpmTolerance"] | 50;
|
131 |
+
newSettings.idle.kp = idle["kp"] | 0.1;
|
132 |
+
newSettings.idle.ki = idle["ki"] | 0.05;
|
133 |
+
newSettings.idle.kd = idle["kd"] | 0.02;
|
134 |
+
}
|
135 |
+
|
136 |
+
newSettings.minAdvance = doc["minAdvance"] | 0;
|
137 |
+
newSettings.maxAdvance = doc["maxAdvance"] | 40;
|
138 |
+
newSettings.injectionTimeMin = doc["injectionTimeMin"] | 1.0;
|
139 |
+
newSettings.injectionTimeMax = doc["injectionTimeMax"] | 20.0;
|
140 |
+
newSettings.idleRpmMin = doc["idleRpmMin"] | 700;
|
141 |
+
newSettings.idleRpmMax = doc["idleRpmMax"] | 900;
|
142 |
+
|
143 |
+
setSettings(newSettings);
|
144 |
+
return true;
|
145 |
+
}
|
146 |
+
|
147 |
+
bool SettingsManager::saveToSD(const char* filename) {
|
148 |
+
File file = SD.open(filename, FILE_WRITE);
|
149 |
+
if (!file) return false;
|
150 |
+
|
151 |
+
String json = exportToJson();
|
152 |
+
file.print(json);
|
153 |
+
file.close();
|
154 |
+
|
155 |
+
return true;
|
156 |
+
}
|
157 |
+
|
158 |
+
bool SettingsManager::loadFromSD(const char* filename) {
|
159 |
+
File file = SD.open(filename);
|
160 |
+
if (!file) return false;
|
161 |
+
|
162 |
+
String json = file.readString();
|
163 |
+
file.close();
|
164 |
+
|
165 |
+
return importFromJson(json);
|
166 |
+
}
|
167 |
+
|
168 |
+
bool SettingsManager::isCompatibleVersion(uint32_t ver) const {
|
169 |
+
// В данной версии поддерживаем только версию 1
|
170 |
+
return ver == 1;
|
171 |
+
}
|
settings_manager.h
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#pragma once
|
2 |
+
#include "Arduino.h"
|
3 |
+
#include "structures.h"
|
4 |
+
#include "SD.h"
|
5 |
+
#include "ArduinoJson.h"
|
6 |
+
|
7 |
+
class SettingsManager {
|
8 |
+
private:
|
9 |
+
SystemSettings settings;
|
10 |
+
static const char* NAMESPACE;
|
11 |
+
static const char* SETTINGS_KEY;
|
12 |
+
uint32_t version;
|
13 |
+
|
14 |
+
void setDefaults();
|
15 |
+
bool validateSettings();
|
16 |
+
|
17 |
+
public:
|
18 |
+
SettingsManager();
|
19 |
+
void begin();
|
20 |
+
bool load();
|
21 |
+
bool save();
|
22 |
+
void reset();
|
23 |
+
|
24 |
+
SystemSettings& getSettings() { return settings; }
|
25 |
+
void setSettings(const SystemSettings& newSettings);
|
26 |
+
|
27 |
+
String exportToJson();
|
28 |
+
bool importFromJson(const String& json);
|
29 |
+
|
30 |
+
bool saveToSD(const char* filename);
|
31 |
+
bool loadFromSD(const char* filename);
|
32 |
+
|
33 |
+
uint32_t getVersion() const { return version; }
|
34 |
+
bool isCompatibleVersion(uint32_t version) const;
|
35 |
+
};
|
structures.h
ADDED
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef STRUCTURES_H
|
2 |
+
#define STRUCTURES_H
|
3 |
+
|
4 |
+
#include <Arduino.h>
|
5 |
+
#include <WebServer.h>
|
6 |
+
#include <Ticker.h>
|
7 |
+
#include <Preferences.h>
|
8 |
+
|
9 |
+
struct CylinderState {
|
10 |
+
unsigned long lastSparkTime;
|
11 |
+
bool isInjecting;
|
12 |
+
uint8_t pin;
|
13 |
+
float advance;
|
14 |
+
float detonationLevel;
|
15 |
+
};
|
16 |
+
|
17 |
+
struct EngineError {
|
18 |
+
bool detonationError;
|
19 |
+
bool temperatureError;
|
20 |
+
bool mapError;
|
21 |
+
bool lambdaError;
|
22 |
+
bool throttleError;
|
23 |
+
uint8_t errorCode;
|
24 |
+
};
|
25 |
+
|
26 |
+
struct EngineData {
|
27 |
+
float rpm;
|
28 |
+
float map;
|
29 |
+
float temp;
|
30 |
+
float lambda;
|
31 |
+
float detonationLevel1;
|
32 |
+
float detonationLevel2;
|
33 |
+
float combinedDetonationLevel;
|
34 |
+
};
|
35 |
+
|
36 |
+
struct ThrottleConfig {
|
37 |
+
float minPosition;
|
38 |
+
float maxPosition;
|
39 |
+
float idlePosition;
|
40 |
+
float speedPulseRatio;
|
41 |
+
float ignitionMap[16][16];
|
42 |
+
};
|
43 |
+
|
44 |
+
struct ThrottleLearnData {
|
45 |
+
float idlePositions[16];
|
46 |
+
uint16_t idleSamples[16];
|
47 |
+
float detonationHistory1[10];
|
48 |
+
float detonationHistory2[10];
|
49 |
+
int detonationHistoryIndex1;
|
50 |
+
int detonationHistoryIndex2;
|
51 |
+
};
|
52 |
+
|
53 |
+
struct ThrottleDiagnostics {
|
54 |
+
bool isCalibrated;
|
55 |
+
bool isStuck;
|
56 |
+
bool isOverloaded;
|
57 |
+
float current;
|
58 |
+
float voltage;
|
59 |
+
float detonationCorrection;
|
60 |
+
};
|
61 |
+
|
62 |
+
struct IgnitionMap {
|
63 |
+
float advance[16][16];
|
64 |
+
float rpmAxis[16];
|
65 |
+
float loadAxis[16];
|
66 |
+
float detonationMap[16][16];
|
67 |
+
float lambdaMap[16][16];
|
68 |
+
};
|
69 |
+
|
70 |
+
struct LearningData {
|
71 |
+
float advanceCorrections[16][16];
|
72 |
+
uint32_t samples[16][16];
|
73 |
+
float detonationHistory[100];
|
74 |
+
uint8_t detonationIndex;
|
75 |
+
};
|
76 |
+
|
77 |
+
struct EngineConfig {
|
78 |
+
float idleRPM;
|
79 |
+
float idleRPMCold;
|
80 |
+
float warmupTemp;
|
81 |
+
float maxRPM;
|
82 |
+
float minRPM;
|
83 |
+
float maxAdvance;
|
84 |
+
float minAdvance;
|
85 |
+
float maxInjection;
|
86 |
+
float minInjection;
|
87 |
+
float detonationThreshold;
|
88 |
+
float learningRate;
|
89 |
+
float maxDetonationCorrection;
|
90 |
+
};
|
91 |
+
|
92 |
+
// Структура для хранения конфигурации
|
93 |
+
struct ConfigData {
|
94 |
+
uint8_t version;
|
95 |
+
|
96 |
+
// Базовые таблицы
|
97 |
+
float fuelTable[16][16]; // Базовая таблица топливоподачи
|
98 |
+
float ignitionTable[16][16]; // Базовая таблица УОЗ
|
99 |
+
|
100 |
+
// Таблицы обучения
|
101 |
+
float fuelLearning[16][16]; // Таблица коррекции топливоподачи
|
102 |
+
float ignitionLearning[16][16]; // Таблица коррекции УОЗ
|
103 |
+
|
104 |
+
// Константы впрыска
|
105 |
+
float injectionTimeMin;
|
106 |
+
float injectionTimeMax;
|
107 |
+
float injectionPressure;
|
108 |
+
float injectorFlow;
|
109 |
+
float injectorDeadtime;
|
110 |
+
float injectorVoltage;
|
111 |
+
float injectorCorrection;
|
112 |
+
float startInjectionTime;
|
113 |
+
float startRpmThreshold;
|
114 |
+
|
115 |
+
// Лямбда
|
116 |
+
float lambdaTarget;
|
117 |
+
float lambdaCorrectionMin;
|
118 |
+
float lambdaCorrectionMax;
|
119 |
+
float lambdaCorrectionStep;
|
120 |
+
|
121 |
+
// Холостой ход
|
122 |
+
float idleRpmMin;
|
123 |
+
float idleRpmMax;
|
124 |
+
float idleAdvanceMin;
|
125 |
+
float idleAdvanceMax;
|
126 |
+
float idleAdvanceStep;
|
127 |
+
float idleAdvanceInterval;
|
128 |
+
float idleTargetRpm; // Целевые обороты ХХ
|
129 |
+
float idleKp; // Коэффициент P для ПИД
|
130 |
+
float idleKi; // Коэффициент I для ПИД
|
131 |
+
float idleKd; // Коэффициент D для ПИД
|
132 |
+
|
133 |
+
// Коррекции
|
134 |
+
bool tempCorrectionEnabled;
|
135 |
+
bool lambdaCorrectionEnabled;
|
136 |
+
bool idleAdvanceLearningEnabled;
|
137 |
+
bool throttleAdaptationEnabled;
|
138 |
+
bool hotEngineCorrectionEnabled;
|
139 |
+
bool accelPumpEnabled;
|
140 |
+
|
141 |
+
// Зажигание
|
142 |
+
String ignitionType;
|
143 |
+
int coilCount;
|
144 |
+
float sparkDuration;
|
145 |
+
float maxAdvance;
|
146 |
+
float minAdvance;
|
147 |
+
float mechanicalAdvance;
|
148 |
+
float dwellTime;
|
149 |
+
|
150 |
+
// Температура
|
151 |
+
float coldStartTemp;
|
152 |
+
float hotEngineTemp;
|
153 |
+
|
154 |
+
uint32_t checksum;
|
155 |
+
};
|
156 |
+
|
157 |
+
// Настройки РХХ
|
158 |
+
struct IdleSettings {
|
159 |
+
float targetRPM;
|
160 |
+
float valveMinPosition;
|
161 |
+
float valveMaxPosition;
|
162 |
+
float rpmTolerance;
|
163 |
+
float kp;
|
164 |
+
float ki;
|
165 |
+
float kd;
|
166 |
+
};
|
167 |
+
|
168 |
+
// Системные настройки
|
169 |
+
struct SystemSettings {
|
170 |
+
uint32_t version;
|
171 |
+
IdleSettings idle;
|
172 |
+
float minAdvance;
|
173 |
+
float maxAdvance;
|
174 |
+
float injectionTimeMin;
|
175 |
+
float injectionTimeMax;
|
176 |
+
float idleRpmMin;
|
177 |
+
float idleRpmMax;
|
178 |
+
};
|
179 |
+
|
180 |
+
// Данные обучения
|
181 |
+
struct LearningData2 {
|
182 |
+
float fuelCorrections[16][16];
|
183 |
+
float ignitionCorrections[16][16];
|
184 |
+
uint32_t cellHits[16][16];
|
185 |
+
uint32_t version;
|
186 |
+
};
|
187 |
+
|
188 |
+
#endif
|
web_interface.h
ADDED
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#ifndef WEB_INTERFACE_H
|
2 |
+
#define WEB_INTERFACE_H
|
3 |
+
|
4 |
+
// HTML page for web interface
|
5 |
+
const char* webPage = R"rawliteral(
|
6 |
+
<!DOCTYPE html>
|
7 |
+
<html>
|
8 |
+
<head>
|
9 |
+
<meta charset="UTF-8">
|
10 |
+
<title>Engine Management System</title>
|
11 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
12 |
+
<style>
|
13 |
+
body { font-family: Arial; margin: 20px; }
|
14 |
+
.container { max-width: 1200px; margin: 0 auto; }
|
15 |
+
.card {
|
16 |
+
border: 1px solid #ddd;
|
17 |
+
padding: 15px;
|
18 |
+
margin: 10px 0;
|
19 |
+
border-radius: 5px;
|
20 |
+
}
|
21 |
+
.value { font-size: 24px; font-weight: bold; }
|
22 |
+
.label { font-size: 14px; color: #666; }
|
23 |
+
.progress-bar {
|
24 |
+
width: 100%;
|
25 |
+
height: 20px;
|
26 |
+
background: #eee;
|
27 |
+
border-radius: 10px;
|
28 |
+
overflow: hidden;
|
29 |
+
}
|
30 |
+
.progress {
|
31 |
+
height: 100%;
|
32 |
+
background: #4CAF50;
|
33 |
+
transition: width 0.3s;
|
34 |
+
}
|
35 |
+
button {
|
36 |
+
padding: 8px 16px;
|
37 |
+
margin: 5px;
|
38 |
+
border: none;
|
39 |
+
border-radius: 4px;
|
40 |
+
background: #4CAF50;
|
41 |
+
color: white;
|
42 |
+
cursor: pointer;
|
43 |
+
}
|
44 |
+
button:hover {
|
45 |
+
background: #45a049;
|
46 |
+
}
|
47 |
+
input[type="number"] {
|
48 |
+
padding: 5px;
|
49 |
+
border: 1px solid #ddd;
|
50 |
+
border-radius: 4px;
|
51 |
+
width: 100px;
|
52 |
+
}
|
53 |
+
</style>
|
54 |
+
</head>
|
55 |
+
<body>
|
56 |
+
<div class="container">
|
57 |
+
<h1>Engine Management System</h1>
|
58 |
+
|
59 |
+
<div class="card">
|
60 |
+
<h2>Real-time Parameters</h2>
|
61 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
|
62 |
+
<div>
|
63 |
+
<div class="value" id="rpm">0</div>
|
64 |
+
<div class="label">RPM</div>
|
65 |
+
</div>
|
66 |
+
<div>
|
67 |
+
<div class="value" id="map">0</div>
|
68 |
+
<div class="label">MAP (kPa)</div>
|
69 |
+
</div>
|
70 |
+
<div>
|
71 |
+
<div class="value" id="tps">0</div>
|
72 |
+
<div class="label">TPS (%)</div>
|
73 |
+
</div>
|
74 |
+
<div>
|
75 |
+
<div class="value" id="lambda">0</div>
|
76 |
+
<div class="label">Lambda</div>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
</div>
|
80 |
+
|
81 |
+
<div class="card">
|
82 |
+
<h2>Learning System</h2>
|
83 |
+
<div>
|
84 |
+
<h3>Learning Progress</h3>
|
85 |
+
<div class="progress-bar">
|
86 |
+
<div class="progress" id="fuelProgress" style="width: 0%"></div>
|
87 |
+
</div>
|
88 |
+
<div style="margin-top: 10px;">
|
89 |
+
<button onclick="toggleLearning()" id="learningBtn">Enable Learning</button>
|
90 |
+
<button onclick="resetLearning()">Reset Learning</button>
|
91 |
+
</div>
|
92 |
+
<div style="margin-top: 20px;">
|
93 |
+
<div>
|
94 |
+
<div class="value" id="fuelCorrection">1.00</div>
|
95 |
+
<div class="label">Fuel Correction Factor</div>
|
96 |
+
</div>
|
97 |
+
<div>
|
98 |
+
<div class="value" id="ignitionCorrection">0.0</div>
|
99 |
+
<div class="label">Ignition Correction (deg)</div>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
</div>
|
104 |
+
|
105 |
+
<div class="card">
|
106 |
+
<h2>Idle Control</h2>
|
107 |
+
<div>
|
108 |
+
<div>
|
109 |
+
<div class="value" id="idlePosition">0</div>
|
110 |
+
<div class="label">Idle Valve Position (%)</div>
|
111 |
+
</div>
|
112 |
+
<div>
|
113 |
+
<div class="value" id="targetRPM">800</div>
|
114 |
+
<div class="label">Target RPM</div>
|
115 |
+
</div>
|
116 |
+
<div style="margin-top: 10px;">
|
117 |
+
<input type="number" id="rpmInput" value="800" min="600" max="2000">
|
118 |
+
<button onclick="setTargetRPM()">Set RPM</button>
|
119 |
+
<button onclick="calibrateIdle()">Calibrate</button>
|
120 |
+
</div>
|
121 |
+
</div>
|
122 |
+
</div>
|
123 |
+
|
124 |
+
<div class="card">
|
125 |
+
<h2>Diagnostics</h2>
|
126 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
|
127 |
+
<div>
|
128 |
+
<div class="value" id="knockEvents">0</div>
|
129 |
+
<div class="label">Knock Events</div>
|
130 |
+
</div>
|
131 |
+
<div>
|
132 |
+
<div class="value" id="engineTemp">0</div>
|
133 |
+
<div class="label">Engine Temp (C)</div>
|
134 |
+
</div>
|
135 |
+
<div>
|
136 |
+
<div class="value" id="batteryVoltage">0</div>
|
137 |
+
<div class="label">Battery Voltage (V)</div>
|
138 |
+
</div>
|
139 |
+
</div>
|
140 |
+
</div>
|
141 |
+
|
142 |
+
<div class="card">
|
143 |
+
<h2>Settings</h2>
|
144 |
+
<div>
|
145 |
+
<button onclick="saveSettings()">Save Settings</button>
|
146 |
+
<button onclick="loadSettings()">Load Settings</button>
|
147 |
+
<button onclick="exportSettings()">Export Settings</button>
|
148 |
+
<button onclick="importSettings()">Import Settings</button>
|
149 |
+
</div>
|
150 |
+
</div>
|
151 |
+
</div>
|
152 |
+
|
153 |
+
<script>
|
154 |
+
var rpmData = {
|
155 |
+
x: [],
|
156 |
+
y: [],
|
157 |
+
type: "scatter",
|
158 |
+
name: "RPM"
|
159 |
+
};
|
160 |
+
|
161 |
+
var mapData = {
|
162 |
+
x: [],
|
163 |
+
y: [],
|
164 |
+
type: "scatter",
|
165 |
+
name: "MAP"
|
166 |
+
};
|
167 |
+
|
168 |
+
Plotly.newPlot("rpmChart", [rpmData], {
|
169 |
+
title: "RPM History",
|
170 |
+
xaxis: { title: "Time" },
|
171 |
+
yaxis: { title: "RPM" }
|
172 |
+
});
|
173 |
+
|
174 |
+
Plotly.newPlot("mapChart", [mapData], {
|
175 |
+
title: "MAP History",
|
176 |
+
xaxis: { title: "Time" },
|
177 |
+
yaxis: { title: "kPa" }
|
178 |
+
});
|
179 |
+
|
180 |
+
function updateData() {
|
181 |
+
fetch("/status")
|
182 |
+
.then(response => response.json())
|
183 |
+
.then(data => {
|
184 |
+
document.getElementById("rpm").textContent = Math.round(data.rpm);
|
185 |
+
document.getElementById("map").textContent = data.map.toFixed(1);
|
186 |
+
document.getElementById("tps").textContent = data.tps.toFixed(1);
|
187 |
+
document.getElementById("lambda").textContent = data.lambda.toFixed(2);
|
188 |
+
|
189 |
+
document.getElementById("fuelProgress").style.width = data.learning_progress + "%";
|
190 |
+
document.getElementById("fuelCorrection").textContent = data.fuel_correction.toFixed(2);
|
191 |
+
document.getElementById("ignitionCorrection").textContent = data.ignition_correction.toFixed(1);
|
192 |
+
|
193 |
+
document.getElementById("idlePosition").textContent = data.idle_position.toFixed(1);
|
194 |
+
document.getElementById("targetRPM").textContent = data.target_rpm;
|
195 |
+
|
196 |
+
document.getElementById("knockEvents").textContent = data.knock_events;
|
197 |
+
document.getElementById("engineTemp").textContent = data.temp.toFixed(1);
|
198 |
+
document.getElementById("batteryVoltage").textContent = data.voltage.toFixed(1);
|
199 |
+
|
200 |
+
const now = new Date();
|
201 |
+
Plotly.extendTraces("rpmChart", {
|
202 |
+
x: [[now]],
|
203 |
+
y: [[data.rpm]]
|
204 |
+
}, [0]);
|
205 |
+
|
206 |
+
Plotly.extendTraces("mapChart", {
|
207 |
+
x: [[now]],
|
208 |
+
y: [[data.map]]
|
209 |
+
}, [0]);
|
210 |
+
});
|
211 |
+
}
|
212 |
+
|
213 |
+
function toggleLearning() {
|
214 |
+
fetch("/learning/toggle", { method: "POST" })
|
215 |
+
.then(response => response.json())
|
216 |
+
.then(data => {
|
217 |
+
alert(data.enabled ? "Learning enabled" : "Learning disabled");
|
218 |
+
});
|
219 |
+
}
|
220 |
+
|
221 |
+
function resetLearning() {
|
222 |
+
if (confirm("Are you sure you want to reset learning data?")) {
|
223 |
+
fetch("/learning/reset", { method: "POST" })
|
224 |
+
.then(response => response.json())
|
225 |
+
.then(() => {
|
226 |
+
alert("Learning data reset");
|
227 |
+
});
|
228 |
+
}
|
229 |
+
}
|
230 |
+
|
231 |
+
function setTargetRPM() {
|
232 |
+
const rpm = document.getElementById("rpmInput").value;
|
233 |
+
fetch("/idle/rpm", {
|
234 |
+
method: "POST",
|
235 |
+
headers: {
|
236 |
+
"Content-Type": "application/json",
|
237 |
+
},
|
238 |
+
body: JSON.stringify({ rpm: parseInt(rpm) })
|
239 |
+
});
|
240 |
+
}
|
241 |
+
|
242 |
+
function calibrateIdle() {
|
243 |
+
if (confirm("Start idle calibration?")) {
|
244 |
+
fetch("/idle/calibrate", { method: "POST" })
|
245 |
+
.then(response => response.json())
|
246 |
+
.then(() => {
|
247 |
+
alert("Calibration complete");
|
248 |
+
});
|
249 |
+
}
|
250 |
+
}
|
251 |
+
|
252 |
+
function saveSettings() {
|
253 |
+
fetch("/settings/save", { method: "POST" })
|
254 |
+
.then(response => response.json())
|
255 |
+
.then(() => {
|
256 |
+
alert("Settings saved");
|
257 |
+
});
|
258 |
+
}
|
259 |
+
|
260 |
+
function loadSettings() {
|
261 |
+
fetch("/settings/load")
|
262 |
+
.then(response => response.json())
|
263 |
+
.then(() => {
|
264 |
+
alert("Settings loaded");
|
265 |
+
});
|
266 |
+
}
|
267 |
+
|
268 |
+
function exportSettings() {
|
269 |
+
window.location.href = "/settings/export";
|
270 |
+
}
|
271 |
+
|
272 |
+
function importSettings() {
|
273 |
+
const input = document.createElement("input");
|
274 |
+
input.type = "file";
|
275 |
+
input.onchange = function(e) {
|
276 |
+
const file = e.target.files[0];
|
277 |
+
const formData = new FormData();
|
278 |
+
formData.append("settings", file);
|
279 |
+
fetch("/settings/import", {
|
280 |
+
method: "POST",
|
281 |
+
body: formData
|
282 |
+
})
|
283 |
+
.then(response => response.json())
|
284 |
+
.then(() => {
|
285 |
+
alert("Settings imported");
|
286 |
+
});
|
287 |
+
};
|
288 |
+
input.click();
|
289 |
+
}
|
290 |
+
|
291 |
+
setInterval(updateData, 1000);
|
292 |
+
</script>
|
293 |
+
</body>
|
294 |
+
</html>
|
295 |
+
)rawliteral";
|
296 |
+
|
297 |
+
#endif
|