1.说明
JSBSim是一个用C++实现的开源跨平台飞行器动力学模型软件。Flightgear也采用了JSBSim作为其中的飞行器动力模型之一。JSBSim同样可以作为一个单独的动力学模型软件进行运行,而且能够作为一个代码库进行调用。JSBSim作为单独动力学模型使用时需要通过命令行参数指定飞行器和初始状态,这样做只能实现简单情境下的仿真。本文的目标是将JSBSim作为代码库,编程实现飞行器模型加载,从而实现可以设置输入,获得输出的完整过程仿真。
2.飞行器仿真的基本概念
飞行仿真的目标是模拟真实飞行器的飞行过程。通过模拟的输入到仿真模型中,观察分析其响应特性,从而预测其在现实世界的响应。飞行仿真可以帮助设计飞行器,验证飞行器飞行能力,模拟训练或者是在游戏中给玩家真实的飞行体验。要完成一个完整的飞行仿真,一般需要如下流程:
根据飞行仿真的流程,我们可以提取其中几个关键技术并作简要说明
-
仿真初始化:要建立一个飞行器模型,首先要有这个飞行器的模型数据,包括尺寸结构、气动系数、发动机参数等等,只有准确的模型数据才能实现准确的仿真;然后我们要给出这个飞行器的初始状态,因为仿真计算事实上就是计算飞行器状态的积分过程,我们要给出它的初始状态才能进行下一步的解算。
-
模型解算:模型解算是飞行仿真最复杂的工作,在解算模型时,我们要根据飞行器模型数据结合飞行器飞行状态计算当前的气动力和力矩,然后解算飞行器的六自由度动力学方程和运动学方程。除此之外,我们还要模拟飞行器所处的大气环境、地形高度和地球自转等因素对飞行器的影响。不过好在这些工作JSBSim已经完美地解决了,我们需要关心的是如何利用JSBSim的模型解算能力实现我们的仿真。
-
获得模型输出:模型的一步解算完成之后,就得到了飞行器从当前时刻到下一个仿真步长的状态量,但是飞行器复杂的模型解算对于我们来说相当于一个黑盒,我们还需要从中提取出我们所需的仿真数据,即模型的输出数据。
-
存储输出数据:在得到模型输出数据之后,我们就可以有效利用这些仿真数据完成我们的仿真目标了。如果是为了记录仿真飞行过程并进行分析的话,那就要将这些数据存储到文件中,在仿真完成之后进行画图或者利用其它方法进行分析;而在某些实时模拟仿真中,还可以将得到的数据实时通过网络传输给其它设备或软件,实现对仿真的实时再现。
-
延时:延时是进行实时仿真所必要的一步,因为我们进行模型解算的过程是远远快于实际时间的。为了实现实时仿真,我们必须准确地进行仿真延时,从而使仿真时间和实际时间保持同步,这样才有实时仿真的真实性。
-
获得输入:仿真过程中不可能一直只有飞行器自己在飞,我们在仿真时也要同时输入一些操纵飞行器的指令。对于一个简单的没有飞行控制系统的飞行器来说,我们的模型输入就是飞行器的各个舵面偏转角以及发动机油门;而对于带有飞行控制系统或者自动驾驶仪的飞行器来说,我们要输入的就可能是一些姿态或者高度指令;对于某些更加自主的飞行器来说,我们甚至只需要给出目标就可以了,剩下的就完全靠飞行器自己计算导航指令、舵面偏转角以及油门等,然后完成飞行任务。
3.编程实践
在前面的文章中,我已经使用过JSBSim.exe作为命令行工具,通过它加载script文件或者aircraft文件,创建飞行器模型来进行仿真。但是这样做只能进行一些简单的情景仿真,我没有办法让飞行仿真做到实时接收用户输入,记录数据或者只显示我要的内容。JSBSim的确留下了一些网络输入和网络输出的接口,也许可以实现我想要的效果。但是通过XML文件来执行这些操作又十分繁琐,而且没有完整的教程或则示例,完全不知从何入手。鉴于以上情况,又因为JSBSim代码的确很好用,写程序是一个更直接的方法,所以我就把JSBSim按照《使用VS2015编译JSBSim》中的方法把JSBSim编译成了静态库,然后编码实现调用JSBSim的代码实现仿真过程中的各个关键内容,实现了我自己的飞行器仿真工程。
接下来我们就开始来进行JSBSim编程实践
3.0最简仿真
一段使用JSBSim编程进行仿真的简单代码如下:
jsbsim_script.cpp
#include <FGFDMExec.h>
using namespace std;
int main()
{
JSBSim::FGFDMExec FDMExec;
bool result = true;
string scriptFileName="c1723.xml"//一个script文件的名称
FDMExec.LoadScript(scriptFileName);
while (result) result = FDMExec.Run();
}
从这段代码我们可以看到,调用JSBSim的主要方法是利用FGFDMExec类,通过实例化一个FGFDMExec类,就相当于获得了一个运行JSBSim仿真的工具箱,通过这个工具箱就可以调用JSBSim的大部分功能,实现我们要的仿真目标。
以上代码的编译需要JSBSim库提供支持(JSBSim.lib编译和使用方法参考《使用VS2015编译JSBSim》),代码实现了和命令“JSBSim.exe --scriptname=c1723.xml”一样的效果,都是加载了script文件进行仿真。但是这种情况下仿真的控制仍然是由script文件进行控制的,我们对于初始化、输入和输出的方法都没有较好地控制。我们的目标是能够利用JSBSim代码对所有的仿真环节进行有效地控制或者配置,接下来就按照仿真环节的各个技术要点进行实践演示。
3.1仿真初始化
进行仿真运行的初始化工作包括仿真变量的初始化、模型的初始化以及初始状态设置,下面就每个工作内容的代码进行分析。
3.1.1 仿真变量初始化
运行仿真之前要先初始化对应的仿真变量,这些变量决定了我们要加载的飞行器和运行时的设置,其中最重要的是FDMExec的初始化设置。代码内容如下:
/********************仿真变量的初始化********************/
string AircraftName = "c172x";
SGPath ResetName = SGPath("reset00");
double end_time = 200;
double simulation_rate = 1. / 120.;
string LogOutputName = "c172x.csv";
FGFDMExec* FDMExec;
bool catalog;
//FGTrim* trimmer;
FDMExec = new FGFDMExec(); //FGFDMExec对象实例化
//设置飞行器、引擎、系统的文件路径,方便后面JSBSim寻找对应路径下的文件
FDMExec->SetAircraftPath(SGPath("aircraft"));
FDMExec->SetEnginePath(SGPath("engine"));
FDMExec->SetSystemsPath(SGPath("systems"));
3.1.2模型初始化
模型的初始化的主要工作是利用FDMExec加载aircraft定义文件中的模型数据。加载则是通过调用LoadModel来实现的,实现代码如下:
/********************仿真模型的初始化********************/
//飞行器模型加载
if (!AircraftName.empty())
{
//选择是否在加载和运行时打印飞行器所有的属性
if (!catalog)
FDMExec->SetDebugLevel(0);
if (!FDMExec->LoadModel(SGPath("aircraft"), SGPath("engine"), SGPath("systems"), AircraftName))
{
cerr << " JSBSim could not be started" << endl << endl;
delete FDMExec;
exit(-1);
}
}
else
{
cerr << " AircraftName must be specified" << endl << endl;
delete FDMExec;
exit(-1);
}
以上代码会通过LoadModel方法来AircraftName指定的aircraft,如果没有指定飞行器名称,那么仿真就无法继续运行,所以会报错。其中catalog,是用来选择是否在加载aircraft文件是打印所有属性的一个开关,是一个可选设置。
3.1.3初始状态设置
JSBSim中进行飞行器初始状态设置的工具是FGInitialCondition类,FGFDMExec的GetIC()方法将会返回FGInitialCondition对象的指针,我们就可以利用这个对象指针来进行对应初始状态设置了。
/********************初始状态设置********************/
//获得初始状态设置对象
FGInitialCondition *IC = FDMExec->GetIC();
if (!ResetName.isNull())//通过初始化文件加载初始状态
{
if (!IC->Load(ResetName))
{
delete FDMExec;
cerr << "Initialization unsuccessful" << endl;
exit(-1);
}
}
else//利用FGInitialCondition提供的接口设置初始状态
{
IC->SetUBodyFpsIC(0.0); //u v w机体三轴速度设置
IC->SetVBodyFpsIC(0.0);
IC->SetWBodyFpsIC(0.0);
IC->SetLongitudeDegIC(-95.163839);//经纬度设置
IC->SetLatitudeDegIC(29.593978);
IC->SetPhiDegIC(0.0); //姿态角设置
IC->SetThetaDegIC(0.0);
IC->SetPsiDegIC(0.0);
IC->SetAltitudeAGLFtIC(4.305); //高度设置
IC->SetTerrainElevationFtIC(0.0);//地形高度设置
IC->SetHeadWindKtsIC(0.0); //机头方向风速设置
}
FDMExec->RunIC();将初始状态传递到六自由度模型中
以上代码完成了模型初始状态的设置,其中IC指向的FGInitialCondition对象就是FDMExec的状态设置工具。如果ResetName不为空的话,就可以用IC调用Load方法来加载reset文件,如果ResetName为空的话,就使用IC调用FGInitialCondition提供的状态设置方法来进行状态设置。其中本文示例加载的reset文件内容如下:
reset00.xml
<?xml version="1.0"?>
<initialize name="reset00">
<!--
This file sets up the aircraft to start off
from the runway in preparation for takeoff.
-->
<ubody unit="FT/SEC"> 0.0 </ubody>
<vbody unit="FT/SEC"> 0.0 </vbody>
<wbody unit="FT/SEC"> 0.0 </wbody>
<longitude unit="DEG"> -95.163839 </longitude>
<latitude unit="DEG"> 29.593978 </latitude>
<phi unit="DEG"> 0.0 </phi>
<theta unit="DEG"> 0.0 </theta>
<psi unit="DEG"> 0.0 </psi>
<altitude unit="FT"> 4.305 </altitude>
<elevation unit="FT"> 0.0 </elevation>
<hwind> 0.0 </hwind>
</initialize>
在以上代码所实现的初始化设置中,调用接口设置状态和加载文件的效果是等效的。FGInitialCondition还提供了一系列其他的状态设置接口,如有需要可以参考文档进行调用。
在IC设置完初始状态之后,FDMExec还需要调用RunIC()方法将初始状态传递到FDMExec的六自由度模型中,就完成了初始状态的设置。
3.2建立仿真循环
利用JSBSim建立仿真循环进行仿真解算应该算是最简单的工作,因为复杂工作都由JSBSim完成了。进行仿真解算的主要方法就是利用FDMExec调用Run()方法,在循环中判断结束条件完成仿真即可。在开始仿真循环之前,还需要进行仿真步长的设置,通过FDMExec调用Setdt方法实现。利用JSBSim库建立仿真循环的代码如下:
/********************设置仿真步长********************/
if (simulation_rate < 1.0)
FDMExec->Setdt(simulation_rate);
else
FDMExec->Setdt(1.0 / simulation_rate);
cout << endl << "--------------- JSBSim Execution beginning ... ---------------"<< endl;
/********************开始仿真循环********************/
bool result = true;
while (result && FDMExec->GetSimTime() <= end_time)
{
//仿真解算
result = FDMExec->Run();
}
以上代码就实现了最简单的仿真循环,但是如果想要使用JSBSim建立实时仿真,则需要在循环中添加延时代码或者在实时操作系统中按照实际的步长时间定时调用仿真解算。下方代码就给出了一个在实时操作系统中实现实时仿真的任务代码示例:
void simulation_task()
{
bool result = true;
while (result && FDMExec->GetSimTime() <= end_time)
{
//仿真解算
result = FDMExec->Run();
//实时操作系统延时
OSTimeDelay(step_time);
}
}
虽然Windows系统不是实时系统,但是由于PC的强大性能,我们仍然可以通过有效的延时策略在Windows环境下实现一个伪实时仿真。原理就是在仿真运行时不断计算距离仿真开始时的时间来进行不同长短的延时时间,从而实现仿真时间和实际时间基本同步的效果。其实现代码可以参考JSBSim源码中的JSBSim.cpp的实现,在此不再赘述。
3.3仿真输入设置
仿真过程中的输入模拟的是我们对飞行器的操作,其中包括发动机和操纵舵两种输入。对于具有自动驾驶仪的飞行器来说,我们的输入也包括自动驾驶仪指令。
要使用JSBSim对模型设置输入,需要用到两个类来进行辅助,分别是FGPropulsion和FGFCS。这两个类分别定义了飞行器推进系统的操作接口和飞行控制系统的操作接口。FGFDMExec中对这两个类进行了实例化,并且分别提供了相应的接口来获得对象指针。
FGPropulsion *prop = FDMExec->GetPropulsion();//获得FGPropulsion对象指针
FGFCS * fcs = FDMExec->GetFCS();//获得FGFCS对象指针
利用这两个对象指针,我们就可以对模型的输入进行自由设置了。以下代码给出了几个设置模型输入的实例,配合仿真的运行就可以实现对飞行器的控制。
打开发动机,并设置油门为最大值
//********************************************** 打开飞行器发动机
FGPropulsion *prop = FDMExec->GetPropulsion();
if(prop->GetNumEngines()==1)//如果引擎数量为1则打开该引擎
{
prop->SetActiveEngine(0);//设置引擎0为活动引擎
prop->SetStarter(1);//打开引擎
prop->SetMagnetos(1);
}
FDMExec->GetFCS()->SetThrottleCmd(0, 1);//设置发动机0的油门为1,即启动发动机
设置舵面输入
//******************************FGFCS设置模型输入代码示例
FGFCS * fcs = FDMExec->GetFCS();
fcs->SetDeCmd(ele_cmd_norm); //设置升降舵指令值(-1~1)
fcs->SetDePos(0, ele_cmd_rad); //弧度指令值
fcs->SetDePos(1, ele_cmd_deg); //角度指令值
fcs->SetDePos(2, ele_cmd__norm);//指令值
fcs->SetDaCmd(ail_cmd_norm); //设置副翼指令值(-1~1)
fcs->SetDaPos(0, ail_cmd_rad); //弧度指令值
fcs->SetDaPos(1, ail_cmd_deg); //角度指令值
fcs->SetDaPos(2, ail_cmd__norm);//指令值
fcs->SetDrCmd(rud_cmd_norm); //设置方向舵指令值(-1~1)
fcs->SetDrPos(0, rud_cmd_rad); //弧度指令值
fcs->SetDrPos(1, rud_cmd_deg); //角度指令值
fcs->SetDrPos(2, rud_cmd__norm);//指令值
fcs->SetDfCmd(flap_cmd_norm); //设置襟翼指令值(-1~1)
fcs->SetDfPos(0, flap_cmd_rad); //弧度指令值
fcs->SetDfPos(1, flap_cmd_deg); //角度指令值
fcs->SetDfPos(2, flap_cmd__norm);//指令值
fcs->SetThrottleCmd(engine_id, throttle_cmd);//设置油门指令值(0~1)
fcs->SetGearPos(0); //设置起落架(0-关,1-开)
fcs->SetCBrake(0); //设置中刹车关(0-关,1-开)
fcs->SetLBrake(0); //设置左刹车关(0-关,1-开)
fcs->SetRBrake(0); //设置右刹车关(0-关,1-开)
以上代码给出了打开发动机并设置油门的代码和使用不同接口函数设置飞行器操纵舵面的代码。一般来说,我们在进行飞行器飞行仿真时,需要合理地调用这些代码,并给出合理的指令,才能实现我们的仿真目的。
3.4获得仿真输出
3.4.1输出到文件的设置
在之前的文章《JSBSim使用教程》中的“output设置”就是输出文件的设置方法之一,我们可以通过修改<output>标签来设置输出文件。但是我们同样可以通过代码对输出文件的名称、记录频率进行设置,在此给出通过代码设置输出到文件的设置方法:
//*************************************output设置
FGOutput *output = FDMExec->GetOutput();
//设置记录频率
output->SetRateHz(5);
//通过output指针设置输出文件名
output->SetOutputName(0, LogOutputName);
//通过FDMExec设置输出文件名称
FDMExec->SetOutputFileName(0, LogOutputName);
3.4.2模型数据的获取方法
为了更方便地利用JSBSim仿真过程中的数据,我们需要在仿真运行过程中实时获得相应的模型数据,模型数据可以通过FGAuxiliary提供的接口函数获得。飞行器的模型数据包括飞行器所处大气环境、风速、表速、地速、位置、姿态、姿态变化率等各种数据,具体不再展开,其中大部分数据都可以通过FGAuxiliary提供的接口函数获得,还有一些特殊的数据则需要利用FDMExec提供的接口分别获得GetAtmosphere、FGAccelerations、FGWinds、FGMassBalance、FGAerodynamics、FGInertial、FGGroundReactions、FGExternalReactions、FGBuoyantForces、FGAircraft的对象指针,然后调用相应接口获取所需数据。对象指针的获取方法和使用方法参考如下代码。关于每一个类有哪些调用接口,可以参考对应的头文件或者阅读JSBSim的软件文档《JSBSim Flight Dynamics Model》。
//获得模型输出
FGAuxiliary *auxiliary = FDMExec->GetAuxiliary();
alpha_rad = auxiliary->Getalpha();
//或者直接使用
alpha_rad = FDMExec->GetAuxiliary()->Getalpha();
4.总结
综上,我们对JSBSim的编程使用方法进行了说明和实践,实现了加载飞行器模型并设置输入运行仿真的效果,已经从对JSBSim编程实践无从下手进步到可以有效利用JSBSim代码的程度了。下一步的目标,则是对JSBSim的有效利用了。
关于JSBSim的应用途径,有以下几个想法。首先我们可以将该代码库应用到其他飞行器仿真软件中去,就像FlighGear或者 OpenEaagles那样,将JSBSim作为仿真库嵌入仿真软件中,这样就相当于直接拥有了JSBSim自带的大量模型数据,直接对这些飞行器进行仿真模拟。
JSBSim还有一个用武之地就是嵌入式实时仿真,对于嵌入式实时仿真软件来说,JSBSim的模型解算可以为编写嵌入式程序节约大量时间。不过将JSBSim代码加入嵌入式工程可能需要耗费大量的资源,这个则需要根据需求来了。其实嵌入式仿真模型库最好的选择应该是LaRCsim模型,这也是FlighGear所使用的模型库的一种,有需要的可以去研究一下FlightGear中FDM模块的源码。
资源文件
[1]JSBSimPractice工程文件(下载地址)
参考阅读