W mojej trainzowej SU45 wygląda to następująco (może uda mi się nie pomieszać):
- silniki trakcyjne mają fizykę liczoną tak, jak Maszynowe elektrowozy (parametry fi,Isat,mfi,mIsat dostałem od youBy'a)
- prądnica ma charakterystykę również opracowaną przez youBy'a, która uwzględnia zależność napięcia od prądu obciążenia, prądu wzbudzenia i obrotów
- prąd wzbudzenia jest funkcją napięcia wzbudnicy (proporcjonalne do obrotów) oraz oporu w obwodzie wzbudzenia, ten ostatni jest częściowo regulowany przez regulator Woodwarda, a część oporów jest przełączana przez styczniki pozycji wstępnych sterowane nastawnikiem
I teraz, silnik spalinowy i regulator obrotów:
Nastawnik ustala dla danej pozycji zadane obroty silnika. Podstawowym parametrem silnika jest "
moc brutto" - jest to moc silnika w funkcji obrotów odczytana z charakterystyki - w praktyce dla silnika 2112SSF daje się to z bardzo dobrym przybliżeniem opisać odcinkiem paraboli (w zakresie powyżej 500rpm). Aby uzyskać aktualną moc oddawaną przez silnik na wale, mnożę "moc brutto" przez "gaz", czyli zmienną opisującą relatywne położenie listwy paliwowej względem położenia znamionowego dla danej pozycji - normalnie jest to 1. Od tak wyliczonej mocy odejmowane są wszystkie obciążenia (silniki trakcyjne, maszyny pomocnicze, ogrzewanie, no i opory samego silnika), i wynik tego bilansu określa nam przyspieszenie lub opóźnienie silnika - tzn. przy nadwyżce mocy obroty będą wzrastały, a przy przeciążeniu - spadały. I tutaj wkracza nam regulator Woodwarda, tj. regulator mocy i obrotów. Dąży on do utrzymania obrotów znamionowych dla danej pozycji. W momencie wykrycia odchyłki obrotów silnika od wartości znamionowej zmienia on położenie listwy paliwowej (zmienna "gaz") aby dopasować moc silnika do aktualnego obciążenia. Równolegle, po wykryciu wychylenia listwy paliwowej od pozycji znamionowej (czyli "gaz" != 1), reguluje on wzbudzenie prądnicy (zmieniając wartość regulowanej rezystancji w obwodzie wzbudzenia), tak aby obciążenie odpowiadało mocy znamionowej. Te operacje są wykonywane aż układ nie powróci do stanu równowagi, czyli obroty równe obrotom znamionowym dla danej pozycji, a "gaz" równy 1 - co oznacza że obciążenie odpowiada mocy znamionowej silnika.
Opracowałem również uproszczony model cieplny silnika, z wymianą ciepła
silnik <> woda w głównym obiegu,
silnik <> otoczenie,
silnik <> olej,
olej <> woda w obiegu pomocniczym,
woda w obiegu głównym <> otoczenie (przez chłodnicę 1),
woda w obiegu pomocniczym <> otoczenie (chłodnica 2). Współczynniki wymiany ciepła w chłodnicy są modyfikowane przez załączenie wentylatorów i żaluzji (z uwzględnieniem prędkości obrotowej wentylatora w pierwszym przypadku i prędkości jazdy w drugim), sterowane termostatami.
Fragmenty kodu - starałem się pokomentować, żeby mniej-wiecej było wiadomo o co chodzi:
class dane
{
//opory w obwodzie wzbudzenia
public float RO;
public float R1;
public float R2;
public float R3;
public float R4;
public float R5;
public float RRW;
//nastawy przekaźnika nadmiarowego i przekaźnika bocznikowania
public float Imax;
public float Imin;
public float Iminb;
//parametry paraboli mocy silnika spalinowego
public float a;
public float c;
//parametry silników trakcyjnych
public float fi0, fi1, fi2, fi3, Isat0, Isat1, Isat2, Isat3, mfi0, mfi1, mfi2, mfi3, mIsat0, mIsat1, mIsat2, mIsat3;
};
/ciach/
// zmienne przekaznikow i stycznikow
public bool PZB2,PWB,PCZ,PR,PPT,PZZ,PA,PWZ,PZS,PPR,PT,PUW1,PUW2,PTC1,PTC2,PZB1,PPB1,PPB2,SPL,SWP,SWW,SPP,SP0,SWM,SW1,SW2,SRW1,SRW2,SPW,SWR,PB,SB1,SB2,SB3,RW,SWMx,PWMin = 0;
public int SP1,SP2,SP3,SST,SP4 = 0;
public bool SLB = 0; //stycznik ladowania baterii
//ich PODPIERACZE
public bool PZB2p,PWBp,PCZp,PRp,PPTp,PZZp,PAp,PWZp,PZSp,PPRp,PTp,PUW1p,PUW2p,PTC1p,PTC2p,PZB1p,PPB1p,PPB2p,SPLp,SWPp,SWWp,SPPp,SP0p,SP1p,SP2p,SP3p,SWMp,SW1p,SW2p,SRW1p,SRW2p,SPWp,SWRp;
/ciach/
//termodynamika ;-)
public float Cw = 4.189, qs = 44700, Cs, Qs, Ts, kfs, dTss;
public float kf, kw, kv, gw, gw3, gwmin, gwmax, Ge, Qd, Qch, Qp, dTs, dTch, Twy, Twe, Te, Tsr, gw2, gwmin2, gwmax2, Qch2, dTs2, dTch2, Twy2, Twe2, Tsr2, kf2, To, dTo, Co;
public bool wentylator1, zaluzje1, wentylator2, zaluzje2, obieg, TCH1, TW1, TW2, TCH2, TW3;
/ciach/
// fragment głównej pętli odpowiedzialny za termostaty i przekaźnik awarii (uniemożliwia rozruch/zrzuca obroty na bieg jałowy)
obieg = ((temperatura1 > 71) or (obieg and temperatura1 > 66)) and (silnik > 0 or pompa_wodna == true);
TCH1 = (((temperatura1 > 82) or (TCH1 and temperatura1 > 74)) and (silnik > 0) and !bezpompy and (Vb > 70) and !awaria_chlodzenia) or chlodzenie;
TW1 = (temperatura1 > 90) or (TW1 and temperatura1 > 81);
TW2 = ((silnik != 0) and (temperatura1 > 30)) or (temperatura1 > 40);
TCH2 = (((temperatura2 > 60) or (TCH2 and temperatura2 > 50)) and (silnik > 0)and !bezpompy and (Vb > 70) and !awaria_chlodzenia) or chlodzenie;
TW3 = (temperatura2 > 70) or (TW3 and temperatura2 > 66);
sd.c.PT = (!TW1 and !TW3 and TW2 and !awaria_termostatow) or sd.c.PTp;
sd.c.PPT = !sd.c.PT and !sd.c.PPTp;
sd.c.PA = (!zamkniecie or niedomkniecie or sd.c.PPT) and !sd.c.PAp;
/ciach/
//bilans cieplny silnika
//Ge - ilość paliwa spalonego w danym kroku czasowym - obliczona na podstawie charakterystyk
//Qd - ciepło wytwarzane w silniku - obliczone jako energia spalonego paliwa minus moc użyteczna
thread void termo()
{
float kfo = 25;
for ( ; ;)
{
qs = 44700;
Cw = 4.189;
Co = 1.885;
kw = 0.35;
kv = 0.6;
gwmin = 400;
gwmax = 4000;
gwmin2 = 400;
gwmax2 = 4000;
Cs = 11000;
kfs = 80;
gw = gwmin + (rpm/1500)*(gwmax-gwmin) + (float)pompa_wodna * 1000;
gw3 = gwmin + (rpm/1500)*(gwmax-gwmin);
gw2 = gwmin2 + (rpm/1500)*(gwmax2-gwmin2) + (float)pompa_wodna * 1000;
rpmw = 80 * rpm / ((0.5 * rpm) + 500);
kf = (float)obieg * (((kw * (float)wentylator1)) * rpmw + (kv * ((float)zaluzje1 + 0.7) * Math.Fabs(predkosc) / 3.6)+ 3) + 3;
kf2 = (((kw * (float)wentylator2)) * rpmw + (kv * ((float)zaluzje2 + 0.7) * Math.Fabs(predkosc) / 3.6)) + 3;
Ge = (float)(silnik > 0)*(0.21 * mocsilnika + 12) / 3600;
if (typ=="SP45")
Qp = (float)(podgrzewacz and pompa_wodna and (Twy < 55) and (Twy2 < 55)) * 1000;
else
Qp = (float)(podgrzewacz and pompa_wodna and (Twy < 60) and (Twy2 < 60)) * 1000;
Qd = qs * Ge - obciazenie;
Qs = (Qd - (kfs * (Ts - Tsr)) - (15 * (0.2 + 0.8 * (float)okienko) * (Ts - Te))); //silnik oddaje czesc ciepla do wody chlodzacej, a takze pewna niewielka czesc do otoczenia, modyfikowane przez okienko
dTss = Qs / Cs;
Ts = Ts + (dTss * 0.5);
dTo = ((kfo * (Ts - To)) - (kfs * (0.2 * (float)(cisnienie > 0.01) + 0.1) * (To - Tsr2))) / (gw3 * Co); // olej oddaje ciepło do wody gdy krazy przez wymiennik ciepla == wlaczona pompka lub silnik
To = To + (dTo * 0.5);
dTs = ((kfs * (Ts - Tsr))) / (gw * Cw);
dTs2 = ((kfs * (0.2 * (float)(cisnienie > 0.01) + 0.1) * (To - Tsr2))) / (gw2 * Cw);
Qch = -kf * (Tsr - Te) + Qp;
Qch2 = -kf2 * (Tsr2 - Te) + (80 * (float)(podgrzewacz_zawor == true) * (Twy - Tsr2)); //przy otwartym kurku B mały obieg jest dogrzewany przez duży - stosujemy przy korzystaniu z podgrzewacza oraz w zimie
dTch = Qch / (gw * Cw);
dTch2 = Qch2 / (gw2 * Cw);
Twe = Twy + (dTch * 0.5);
Twy = Twe + (dTs * 0.5);
Tsr = 0.5 * (Twy + Twe);
Twe2 = Twy2 + (dTch2 * 0.5);
Twy2 = Twe2 + (dTs2 * 0.5);
Tsr2 = 0.5 * (Twy2 + Twe2);
temperatura1 = Twy;
temperatura2 = Twy2;
fuelConsumed = fuelConsumed + (Ge * 0.5);
while(fuelConsumed >= 0.83)
{
fuelConsumed = fuelConsumed - 0.83;
fuelQueue.DestroyProductMatching(null,1);
}//if
Sleep(0.5);
}
}
/ciach/
//głowny wątek fizyki
//zmienna "silnik" to określenie fazy pracy silnika. 0 - zgaszony, 1 - normalna praca, 2 - rozruch, faza I, 3 - rozruch, faza II - zapłon.
thread void regulator()
{
float bilans,opory,drpm,arpm,rpm_doc;
for ( ; ; )
{
bool dodajgazu = ((rpm_zadane > 696) and (pozycja < (pozycja_zadana - 1)));
// sprawdzenie mocy i regulacja napiecia
prad_razem = prad_silnika[1] + prad_silnika[2] + prad_silnika[3] + prad_silnika[4] + prad_silnika[5] + prad_silnika[6];
moc = napiecie * Math.Fabs(prad_razem);
if ((gaz > 0.03 and !dodajgazu) or PZB1 == 1 or nastawnik < 2)
{
if (PZB1 == 1)
woodward = woodward - 0.04;
if (PZB1 == 0)
woodward = woodward - 0.002;
}
if ((gaz < 0 or dodajgazu) and PZB1 == 0)
woodward = woodward + 0.005;
if (woodward < 0)
woodward = 0;
if (woodward > 1)
woodward = 1;
//sterowanie bocznikami:
SWMx = (woodward > 0.98) and !PWB and SPL and SLB;
PWMin = (woodward < 0.003) or (PWMin and (woodward < 0.01));
PB = (Math.Fabs(prad_silnika[3]) > parametry.Imin or (PB and Math.Fabs(prad_silnika[3]) > parametry.Iminb));
bool PZB1pom = PZB1 or SWMx;
PWB = (PB or nastawnik < 11 or !SPL) and !sd.c.PWBp and nastawnik != 0;
PZB1 = ((sd.c.bocznik_enable == 1) and !PWB and !PZB2 and PZB1pom and !SB3 and (sd.c.OSW == 3) and (sd.c.OST == 0)) and SPL and (!SB1 or (SB1 and PPB1 and !SB2) or (SB2 and PPB2));
PZB2 = (PWMin and !PWB and SPL) or sd.c.PZB2p;
PPB1 = (predkosc > 11.11) and nastawnik > 7 ;
PPB2 = (predkosc > 13.88);
SB3 = SB1 and SB2 and (SB3 or (PZB2 and PPB2)) and !PWB and SPL and PPB2;
SB2 = SB1 and (SB2 or (PZB2 and PPB1)) and (PPB2 or !PWB) and SPL and PPB1;
SB1 = (SB1 or PZB2) and (PPB1 or !PWB);
//wyliczenie mocy na wale silnika:
if (rpm > 550)
{
mocbrutto = 0.15 * rpm + (((parametry.a * rpm * rpm) + parametry.c) / 1.36); //parabola mocy, działa w zakresie obrotów roboczych
}
if (rpm < 551)
{
mocbrutto = 0.029 * rpm; //dla zakresu poniżej, przybliżenie liniowe. W zasadzie ma znaczenie tylko przy rozruchu
}
mocsilnika = (1 + gaz) * mocbrutto;
if (silnik == 0)
obciazenie = 0;
if (silnik == 1)
obciazenie = ((moc / 950) + mocgrzania + 70);
if (silnik > 1 and rpm_zadane > 0)
obciazenie = 80;
mocrozrusznika = (float)(silnik == 2) * 0.04 * rpm;
if (silnik == 2 and rpm_zadane > 100) //obliczenie rezystancji zastępczej prądnicy w trakcie rozruchu - używane do obliczenia prądu rozruchowego i rozładowania baterii
Rroz = 0.1 + 0.0002 * rpm;
else
Rroz = 99999;
if (rpm > 0 and silnik == 0)
rpm = rpm - 1;
if (rpm < rpm_docelowe and silnik == 2)
rpm = rpm + 2;
if (silnik == 2 and rpm_zadane > 0)
rpm = rpm + 0.4;
if (silnik == 3 and rpm < rpm_docelowe)
rpm = rpm + 10;
bilans = (mocsilnika - obciazenie + mocrozrusznika);
opory = 0.15 * rpm;
arpm = bilans - opory;
drpm = arpm * 1.5;
if (silnik == 1)
rpm = rpm + 0.05 * drpm; //0.05 - dt, czyli krok czasowy tej pętli
if (rpm < 0)
rpm = 0;
if (silnik == 1 and rpm < 600)
rpm = 600;
rpm_docelowe = rpm_zadane + 1.5 * (mocsilnika - obciazenie + mocrozrusznika);
rpm_doc = Math.Fmin(rpm+(rpm_zadane/(rpm+0.01))*65,rpm_zadane);
if (mocbrutto > 0)
dgaz = ((obciazenie + 0.15 * rpm_doc * (rpm_doc/(rpm+0.01)))/mocbrutto) - 1;
if (Math.Fabs(rpm_zadane - rpm) > 40) //przy dużych odchyłkach regulator zmienia dawkę paliwa szybciej.
gaz = 0.5 * gaz + 0.5 * dgaz;
else
gaz = 0.9*gaz + 0.1 * dgaz;
if (gaz < -1)
gaz = -1;
if (gaz > 1)
gaz = 1;
/ciach/
// wyliczenie pradu
float prad_nowy;
for (silniki = 6; silniki > 0; silniki--)
{
// prądnica
if (SPL and nastawnik > 1 and baterie and WSG and WSW)
{
Uw = (190 * rpm) / 1500;
if (rpm > 0)
pIsat = 8 * 1500 / rpm;
RRW = parametry.RRW * (1 - woodward);
RWO = (parametry.R1 * (1 - SP3)) + (parametry.R2 * (1 - SP2)) + (parametry.R3 * (1 - SP1)) + (parametry.R4 * SP4) + (parametry.R5 * SST);
Rp = parametry.RO + RWO + RRW + 3.56;
pfi = 0.88;
I0 = Uw / Rp;
if ((I0 - aS * (prad_razem * nawrotnik) + pIsat) != 0)
sem = (rpm * pfi * (I0 + Ir - aS * (prad_razem * nawrotnik)) / (I0 - aS * (prad_razem * nawrotnik) + pIsat));
else
{
sem = 0;
PN[silniki] = true;
prad_silnika[silniki] = 0;
SPL = false;
}
napiecie = 0.9 * napiecie + 0.1 * sem;
// silniki trakcyjne
Isf = nawrotnik * Isat;
U = nawrotnik * napiecie;
R = 0.2;
b = R * Isf + Mn * fi * obroty - U;
delta = b * b + 4 * U * Isf * R;
if (delta >= 0)
prad_nowy = (-1 * b + nawrotnik * Math.Sqrt(delta)) / (2 * R);
}
else
{
sem = 0;
napiecie = sem;
prad_nowy = 0;
}
prad_silnika[silniki] = (0.9 * prad_silnika[silniki] + 0.1 * prad_nowy) * (float)(SL[silniki] and SPL);
if (Math.Fabs(prad_silnika[silniki]) > parametry.Imax or (awaria_silnika[silniki] == true and SE[silniki] == true)) {
PN[silniki] = true;
prad_silnika[silniki] = 0;
SPL = false;
}
moment[silniki] = mfi * Math.Fabs(prad_silnika[silniki]) * (1 - 1/((Math.Fabs(prad_silnika[silniki]) / mIsat)+1));
}
//wyliczanie sily
sila = Math.Fabs(2 * (moment[1] + moment[2] + moment[3] + moment[4] + moment[5] + moment[6])/(1000 * 1.1 * 0.29));
prad = prad_silnika[6]; //dla amperomierza w szafie
/ciach/
Sleep(0.05);
}
}
- tak to wygląda w praktyce, nagrywane parę dni temu. Widać tu np. chwilowe spadki obrotów w momencie załączania boczników - co oznacza skokowy wzrost obciążenia, bo jest podparty przekaźnik PZB2, więc nie ma odwzbudzenia prądnicy przed załączeniem bocznika. Gdybym jechał bez podparcia, z pełną automatyką bocznikowania, to z kolei w momentach załączania boczników miałbym spadek napięcia do wartości minimalnej i chwilowy podskok obrotów.