Files

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