119 lines
5.4 KiB
C++
119 lines
5.4 KiB
C++
#pragma once
|
|
#include "FluidSim/MachinePorts.hpp"
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace fluidsim::port {
|
|
|
|
// Accepts only coherent, current channels from one physical crank pickup.
|
|
// Signed reverse measurements remain valid sensors, but cannot drive this
|
|
// forward-only ignition controller. A stationary crank is valid.
|
|
std::optional<SignalTiming> ecu_crank_sample(const SignalValue& speed,
|
|
const SignalValue& angle,double time);
|
|
|
|
class CrankIgnitionClock {
|
|
public:
|
|
struct Result {bool valid{};unsigned sparks{};};
|
|
// Crank-only wasted spark: one event per revolution at the configured
|
|
// pickup angle, without consulting a cylinder's hidden 720-degree phase.
|
|
Result sample(const SignalValue& speed,const SignalValue& angle,
|
|
double time,double advance_degrees,double tdc_degrees=0);
|
|
void reset(){previous_.reset();last_event_cycle_.reset();}
|
|
private:
|
|
std::optional<SignalTiming> previous_;
|
|
std::optional<double> last_event_cycle_;
|
|
};
|
|
|
|
// Ideal sensors and injectors; calibration parameters are not engine measurements.
|
|
struct EcuConfig {
|
|
enum class IdleControl { Feedback, FixedOpening };
|
|
IdleControl idle_control{IdleControl::Feedback};
|
|
struct IgnitionPoint { float rpm, degrees_btdc; };
|
|
std::vector<IgnitionPoint> ignition_map{{0, 10}, {15000, 40}};
|
|
float idle_rpm{1800.0f};
|
|
float idle_base_opening{0.08f}, idle_max_opening{0.25f};
|
|
float idle_kp{0.0001f}, idle_ki{0.00004f};
|
|
float throttle_time_constant{0.001f}, throttle_rate{1000.0f}; // seconds, full travel/second
|
|
// Zero uses the opening calibration, preserving symmetric response.
|
|
float throttle_closing_time_constant{}, throttle_closing_rate{};
|
|
float rev_limit_rpm{10000.0f}, rev_resume_rpm{9750.0f};
|
|
bool rev_limit_enabled{true};
|
|
bool rev_limit_fuel_cut_enabled{true};
|
|
bool rev_limit_ignition_cut_enabled{true};
|
|
float rev_limit_cut_time{}; // Minimum cut duration in seconds, followed by RPM hysteresis.
|
|
float overrun_cut_rpm{3500.0f}, overrun_resume_rpm{2400.0f};
|
|
bool overrun_fuel_cut_enabled{true};
|
|
bool overrun_ignition_cut_enabled{false};
|
|
float idle_spark_gain{0.0f}, idle_minimum_advance{-5.0f}; // degrees/RPM, degrees BTDC
|
|
float stoichiometric_afr{14.7f}, power_afr{12.8f};
|
|
|
|
};
|
|
|
|
class Ecu : public EcuConfig {
|
|
public:
|
|
void step(float dt, float pedal, float rpm);
|
|
float fuel_for_air(float trapped_air_mass) const;
|
|
float throttle_opening() const { return opening_; }
|
|
float throttle_target() const { return target_; }
|
|
float target_afr() const { return afr_; }
|
|
float spark_advance() const { return spark_; }
|
|
bool fuel_cut() const { return (limiter_ && rev_limit_fuel_cut_enabled) || (overrun_ && overrun_fuel_cut_enabled) || fault_; }
|
|
bool ignition_cut() const { return (limiter_ && rev_limit_ignition_cut_enabled) || (overrun_ && overrun_ignition_cut_enabled) || fault_; }
|
|
bool rev_limited() const { return limiter_; }
|
|
bool idle_active() const { return idle_; }
|
|
bool sensor_fault() const { return fault_; }
|
|
|
|
private:
|
|
float filtered_rpm_{};
|
|
bool initialized_{};
|
|
float opening_{}, target_{}, integral_{}, afr_{14.7f}, spark_{10.0f};
|
|
bool limiter_{}, overrun_{}, fault_{}, idle_{};
|
|
float limiter_time_{};
|
|
};
|
|
|
|
// Separate from the legacy ECU: measurements are supplied through typed wires.
|
|
// Alpha-N cells are calibrated kg per cylinder per four-stroke cycle, indexed
|
|
// by RPM rows and measured TPS columns. No trapped-air-mass input is available.
|
|
struct SensorEcuConfig {
|
|
enum class Strategy {Ignition,AlphaN,SpeedDensity};
|
|
Strategy strategy{Strategy::Ignition};
|
|
std::vector<EcuConfig::IgnitionPoint> ignition_map{{0,10},{6000,30}};
|
|
std::vector<double> fuel_rpm{0,6000},fuel_tps{0,1};
|
|
std::vector<double> fuel_per_cycle{0.000002,0.00002,0.000002,0.00002};
|
|
// Open-loop dry ideal-gas estimate per cylinder. Geometry, VE and fuel AFR
|
|
// are calibration; MAP and IAT must arrive as current wired measurements.
|
|
double displacement{.0003},air_gas_constant{287},target_afr{14.7};
|
|
std::vector<double> ve_rpm{0,6000},ve_pressure{20000,100000};
|
|
std::vector<double> volumetric_efficiency{.9,.9,.9,.9};
|
|
bool rev_limit_enabled{true};
|
|
double rev_limit_rpm{7000},rev_resume_rpm{6800};
|
|
};
|
|
inline constexpr int sensor_ecu_strategy_count=3;
|
|
const char* sensor_ecu_strategy_key(SensorEcuConfig::Strategy);
|
|
const char* sensor_ecu_strategy_name(SensorEcuConfig::Strategy);
|
|
SensorEcuConfig::Strategy sensor_ecu_strategy_from_key(std::string_view);
|
|
SensorEcuConfig::Strategy next_sensor_ecu_strategy(SensorEcuConfig::Strategy);
|
|
struct SensorEcuMeasurements {SignalValue speed,angle,tps,map,iat;};
|
|
struct SensorEcuState {
|
|
bool sensor_fault{true},rev_limited{};
|
|
double spark_advance{},fuel_per_cycle{},fuel_mass_flow{};
|
|
double estimated_air_per_cycle{},volumetric_efficiency{};
|
|
unsigned spark_events{};
|
|
};
|
|
class SensorEcu {
|
|
public:
|
|
explicit SensorEcu(SensorEcuConfig config);
|
|
void apply_calibration(SensorEcuConfig config);
|
|
const SensorEcuState& sample(const SignalValue& speed,const SignalValue& angle,
|
|
const SignalValue& tps,double time);
|
|
const SensorEcuState& sample(const SensorEcuMeasurements&,double time);
|
|
const SensorEcuState& state()const{return state_;}
|
|
void reset(){clock_.reset();state_={};limiter_=false;}
|
|
private:
|
|
SensorEcuConfig config_;
|
|
CrankIgnitionClock clock_;
|
|
SensorEcuState state_;
|
|
bool limiter_{};
|
|
};
|
|
} // namespace fluidsim::port
|