背景
项目需要用到SIMULINK转C,不得不摸索一下SIMULINK仿真工具。这个过程中涉及到一些概念,比如连续时间系统和离散时间系统,真让我头大,归其原因还是自身基础不牢,应用经验不足。
作为使用者,我的目标其实不是要弄清楚过多的算法和过多的原理,而是要重点梳理使用的逻辑,这样才能了解使用过程存在的潜在风险点。
围绕着这个目标我翻阅了微积分、信号与系统等课程,再次了解了泰勒展开、S变换、Z变换,经过了若干个混沌的夜晚,最终在克服了拿来主义的思想后,搭建了个简单仿真模型,才有了点具象的感受,在此做个总结,此文概念不一定准确,描述不一定正确,但对于我来说却有比较直观的感受,大拿们不要见笑。
怎么理解这几个概念
"什么叫连续系统仿真和离散系统仿真?
有网友说,连续系统仿真输出波形是平滑的,离散系统仿真波形是锯齿的
有网友说,计算机进行的仿真时间上都是离散的,还说连续系统仿真是解常微分方程,不依赖于前一状态,离散系统仿真则是依赖于解差分方程,必须知道之前的状态
这种概念在网上一搜一大把,懂的人是都懂,不懂的人是真不理解啊!!
从简单的仿真出发
连续时间仿真
图1是一个简单的仿真模型,simdata来源于workspace,simout存储至workspace。图2是子系统,一个最简单的积分器,这是一个连续时间系统,仿真步长为0.0002s。图3是仿真结果。
图1 仿真模型
图2 连续时间系统积分器
图3 连续时间系统仿真结果
离散时间仿真
图4 离散化处理
图4 离散化后的积分器
图5 离散化后的仿真波形
问题的摸索
连续波形和锯齿波形
确实如网友所说,连续系统的仿真波形是平滑的,离散系统的仿真波形是锯齿的,但这是真的么?
从workspace的仿真输出结果来看,不论是离散系统,还是连续系统,其仿真输出都是离散的点,步长都是仿真步长0.0002s,那说明连续系统的仿真波形也不是物理世界的连续的呀,为什么画出来的波形一个是连续的,一个是锯齿的呢?
图6
原来啊,仿真输出的结果使用的timeseries结构体,它有一个成员变量叫DataInfo,连续时间系统DataInfo的值是linear(线性插值),而离散时间系统DataInfo的值是ZOH(零阶保持,这个在离散化时可以设置),这就导致MATLAB画图时波形是不一样的。
分析到这里我总算理解了,为什么仿真结果都是离散的点,而仿真波形一个看起来是连续平滑的,一个看起来是锯齿的呢。
差分方程和微分方程
既然连续时间系统在计算机系统中的仿真也是离散的,那连续时间系统的“连续”又是指的什么呢?其实,连续时间系统和离散时间系统的“连续”和“离散”指的计算方法,确实如网友所说 连续时间系统的仿真计算基于微分方程,而离散时间系统的仿真计算基于差分方程。
这一点很多网友都提到了,并且网上一推资料在讲连续系统仿真的微分方程的,比如欧拉、龙格-库塔等等,但对于我这种入门级选手来说,总感受始终没有点破。
为了弄清楚此事,这里我们来看一下连续系统和离散系统转成C代码后的主要差别。
连续系统积分环节和离散积分环节转C代码
代码框架一致
通过定时器或中断每仿真步长触发一次计算
计算方法不一致
离散系统(左侧)采用的是差分计算方法,而连续系统(右侧)采用的是微分计算方法
在微分求解器中,有MajorTimeStep步长和MinorTimeStep步长两个概念,MajorTimeStep步长即仿真步长(这里是0.0002s),而MINOR步长则需要根据具体求解器来,对于本仿真采用的ODE3求解器来说,在每个MajorTimeStep下,实际上执行了三次MinorTimeStep步长的计算,这个概念在离散时间系统中是没有的,离散时间系统下每一个仿真步长只会计算一次。
具体来看求解器函数rt_ertODEUpdateContinuousStates,发现wxhtest_derivatives函数执行了三次,这是由ODE3求解器确定的,这个求解器的相关参数定义在rt_ODE3_A和rt_ODE3_B中,为什么这个参数可以用于求解,这个时候就需要去了解欧拉、龙格库塔这些个远古大神了。
这一顿操作下来,当前仿真步长下的值就计算完成了。
static void rt_ertODEUpdateContinuousStates(RTWSolverInfo *si )
{
/* Solver Matrices */
static const real_T rt_ODE3_A[3] = {
1.0/2.0, 3.0/4.0, 1.0
};
static const real_T rt_ODE3_B[3][3] = {
{ 1.0/2.0, 0.0, 0.0 },
{ 0.0, 3.0/4.0, 0.0 },
{ 2.0/9.0, 1.0/3.0, 4.0/9.0 }
};
time_T t = rtsiGetT(si);
time_T tnew = rtsiGetSolverStopTime(si);
time_T h = rtsiGetStepSize(si);
real_T *x = rtsiGetContStates(si);
ODE3_IntgData *id = (ODE3_IntgData *)rtsiGetSolverData(si);
real_T *y = id->y;
real_T *f0 = id->f[0];
real_T *f1 = id->f[1];
real_T *f2 = id->f[2];
real_T hB[3];
int_T i;
int_T nXc = 1;
rtsiSetSimTimeStep(si,MINOR_TIME_STEP);
/* Save the state values at time t in y, we'll use x as ynew. */
(void) memcpy(y, x,
(uint_T)nXc*sizeof(real_T));
/* Assumes that rtsiSetT and ModelOutputs are up-to-date */
/* f0 = f(t,y) */
//第一次计算,时刻为0
rtsiSetdX(si, f0);
wxhtest_derivatives();
/* f(:,2) = feval(odefile, t + hA(1), y + f*hB(:,1), args(:)(*)); */
hB[0] = h * rt_ODE3_B[0][0];
for (i = 0; i < nXc; i++) {
x[i] = y[i] + (f0[i]*hB[0]);
}
//第二次计算,时刻为0 + 0.0002*0.5 = 0.0001
rtsiSetT(si, t + h*rt_ODE3_A[0]);
rtsiSetdX(si, f1);
wxhtest_step();
wxhtest_derivatives();
/* f(:,3) = feval(odefile, t + hA(2), y + f*hB(:,2), args(:)(*)); */
for (i = 0; i <= 1; i++) {
hB[i] = h * rt_ODE3_B[1][i];
}
for (i = 0; i < nXc; i++) {
x[i] = y[i] + (f0[i]*hB[0] + f1[i]*hB[1]);
}
//第三次计算,时刻为0 + 0.0002*0.75 = 0.00015
rtsiSetT(si, t + h*rt_ODE3_A[1]);
rtsiSetdX(si, f2);
wxhtest_step();
wxhtest_derivatives();
/* tnew = t + hA(3);
ynew = y + f*hB(:,3); */
for (i = 0; i <= 2; i++) {
hB[i] = h * rt_ODE3_B[2][i];
}
for (i = 0; i < nXc; i++) {
x[i] = y[i] + (f0[i]*hB[0] + f1[i]*hB[1] + f2[i]*hB[2]);
}
rtsiSetT(si, tnew);
rtsiSetSimTimeStep(si,MAJOR_TIME_STEP);
}
分析到这里我总算摸到了 “连续系统仿真基于微分方程、离散系统仿真基于差分方程”这句话的样子了,过程很艰难,但真是大快人心呀!
再次追问基本概念
前文已经提到了,连续时间系统和离散时间系统的“连续”和“离散”指的计算方法不同,物理世界是连续的,而计算机只能是离散的,那为什么采用ODE3求解器或是其它的微分求解器的“离散点计算”能被称之连续时间系统仿真呢?
我想这又是一个基础概念的问题,因为ODE3求解器的理论基础就是在某一点N阶可导,可导即连续,这样才有局部线性化、泰勒公式、微分方程、欧拉方程、龙格-库塔等等不断延伸的概念和方法。。。。
反思一下
应该说大多数工程师在使用各种工具时往往只专注于会用,但这样在工程中常常遇到各种问题,比如文章的这些最基本的概念实际上大学课程都有学过,但一旦用起来才发现其实都不懂,我也在反思,大学里面都是从概念讲起,弱化了应用,应付了考试,其实没有真正理解,而工程上从应用开始,碰到问题后再继续回溯,最终又回到了基础概念。所以啊,解决一切问题的基础就是追本溯源,夯实基础,强化应用。