Simulink集成C/C++遗留代码:S-Function与Legacy Code Tool实战指南

Simulink集成C/C++遗留代码:S-Function与Legacy Code Tool实战指南

1. 项目概述:当旧代码遇上新模型

在嵌入式系统、控制算法乃至汽车电子这些领域摸爬滚打久了,你手头总会积攒下一些“祖传”的C/C++代码。这些代码可能是经过无数次现场测试验证的经典算法,也可能是与特定硬件深度绑定的驱动库,它们稳定、可靠,但往往也伴随着一个共同的特点:与现代的模型化设计工具,比如Simulink,显得有些格格不入。直接把这些代码扔了重写?成本高风险大,而且可能引入新的未知错误。完全用Simulink的模块重搭一遍?对于复杂的逻辑和算法,这几乎是一项不可能完成的任务,而且失去了原有代码的“灵魂”。

“Incorporate legacy code into Simulink”(将遗留代码集成到Simulink中)这个需求,恰恰是解决这个矛盾的钥匙。它不是一个简单的“导入”动作,而是一套完整的工程实践,目标是在保留原有代码核心价值的同时,将其无缝融入基于模型的现代设计流程。这意味着,你的旧算法可以在Simulink的图形化环境中被调用、仿真、调试,甚至通过Real-Time Workshop(现为Simulink Coder/Embedded Coder)自动生成产品级代码,与模型中新开发的部分协同工作。这背后的核心技术,绕不开S-Functions(系统函数)。你可以把它理解为一个“适配器”或“黑盒包装器”,Simulink通过它来理解并执行你的外部代码。

为什么这件事如此重要?首先,它保护了既有投资,避免了重复造轮子。其次,它极大地加速了系统集成与验证过程。你可以在Simulink的仿真环境中,直观地看到旧代码与新模型交互的时序、数据流和动态响应,提前发现集成问题。最后,它为后续的代码生成、硬件在环测试乃至产品部署铺平了道路,是实现从模型到代码(MBD)完整闭环的关键一步。无论你是算法工程师、软件工程师还是系统架构师,掌握这项技能都能让你在处理新旧技术栈融合时游刃有余。

2. 核心思路与方案选型:不止于S-Function

把遗留代码塞进Simulink,听起来目标明确,但路径却有好几条。选择哪条路,直接决定了后续的开发效率、运行性能和维护成本。我们不能一上来就埋头写S-Function,而是要先从全局视角审视一下手头的“遗产”和项目需求。

2.1 评估你的“遗产”代码

在动手之前,必须像考古学家一样仔细审视你的遗留代码。这决定了集成策略的起点。

  1. 代码结构与功能:这是一段独立的算法函数(如一个PID控制器、一个滤波函数),还是一个完整的、有状态机的程序循环?函数接口是否清晰(输入、输出、参数明确)?代码是否依赖全局变量或静态变量来维持状态?清晰的函数式接口最容易集成,而重度依赖全局状态或复杂数据结构的代码则挑战更大。
  2. 外部依赖:代码是否调用了特定的第三方库(如数学库、硬件驱动库)或操作系统API(如文件操作、线程)?这些依赖在目标仿真或部署环境中是否可用?例如,一段用了Windows特定API的代码,想在Linux上跑的Simulink Real-Time目标里运行,就需要额外处理。
  3. 实时性要求:代码对执行时序敏感吗?在最终生成的嵌入式代码中,是否需要保证确定的执行时间或中断响应?这会影响你在S-Function中选择离散状态还是连续状态,以及采样时间的设置。
  4. 测试完备性:原有代码是否有完整的单元测试?这能为你集成后的验证提供宝贵的基准。如果没有,你可能需要先在外部环境(如简单的C测试程序)中构建一些测试用例,作为后续在Simulink中验证的“黄金标准”。

2.2 主要集成路径深度对比

Simulink提供了多种集成机制,每种都有其适用场景和优缺点。

集成方式核心原理适用场景优点缺点与注意事项
C MEX S-Function编写C语言文件,实现一组预定义的回调函数(如mdlInitializeSizes,mdlOutputs),编译成MEX文件供Simulink调用。最通用、最强大。适用于复杂算法、需要精细控制内存/状态、有高性能要求、或需要与底层硬件/库交互的场景。功能完整,可完全控制算法执行流程、内存管理和数据类型。支持所有Simulink特性(如可变步长求解器)。性能最优。开发复杂度最高,需要深入理解Simulink仿真循环。手动管理内存易出错。调试相对困难(需结合printf和调试器)。
Legacy Code Tool (LCT)提供一个MATLAB脚本接口,通过描述旧代码的接口(函数名、输入/输出参数、头文件等),自动生成包装它的C MEX S-Function和TLC文件。集成现有、接口清晰的C函数的首选。代码本身无需改动,通过声明式配置完成集成。大幅降低开发门槛。自动化生成,减少手写错误。自动生成TLC文件,支持代码生成。维护方便,配置即代码。灵活性不如手写S-Function。对于特别复杂的数据类型(如嵌套结构体、动态数组)或非标准的调用约定支持可能有限。
MATLAB Function Block在Simulink中直接使用MATLAB语言编写算法。可通过coder.extrinsic声明调用外部.m文件,或使用coder.ceval直接内联调用C代码。快速原型验证,算法逻辑用MATLAB表达更自然。或用于包装简单的、无需代码生成的MATLAB脚本。开发速度快,利用MATLAB丰富的数学库和调试工具。适合算法探索阶段。仿真速度通常慢于C MEX。通过coder.extrinsic调用的函数不支持代码生成,仅用于仿真。coder.ceval要求较高,需处理数据类型转换。
System Object面向对象的框架,用于实现具有状态、且每次调用需执行多个步骤的算法。可同时用于Simulink和MATLAB。实现复杂的、有状态的流式处理算法(如通信系统中的编码器、滤波器组)。面向对象,封装性好。支持MATLAB和Simulink统一接口。自带代码生成支持。学习曲线较陡。对于简单的函数式遗留代码,可能显得“杀鸡用牛刀”。
调用外部可执行文件使用Simulink的“System Command”模块或S-Function启动外部进程进行数据交换。集成一个完全独立、无法修改的“黑盒”可执行程序,或进行软件在环协同仿真。完全隔离,互不影响。可集成任何语言编写的程序。仿真效率极低,进程间通信开销大。时序同步困难。不支持代码生成,仅用于特定仿真验证。

注意:对于最终需要生成嵌入式代码的项目,C MEX S-Function(手写或通过LCT生成)是唯一的生产级选择。MATLAB Function Block若涉及extrinsic调用或解释执行,只能停留在仿真阶段。

2.3 决策流程图:我该选哪条路?

面对具体项目,你可以遵循以下决策流程:

  1. 目标是什么?仅用于桌面仿真验证,还是最终要生成产品代码?
    • 仅仿真:可以考虑MATLAB Function Block或外部可执行文件,以快速实现为目标。
    • 代码生成:必须使用C MEX S-Function或Legacy Code Tool。
  2. 代码形态是什么?是清晰的C函数,还是混乱的“意大利面条”代码?
    • 清晰函数:优先尝试Legacy Code Tool,它能解决80%的集成问题。
    • 复杂状态/结构:可能需要手写C MEX S-Function以获得完全控制权。
  3. 是否有性能要求?仿真规模大或算法计算密集?
    • :C MEX S-Function性能最佳。
    • :其他方式也可接受。

实操心得:在大型项目中,我通常采用混合策略。先用Legacy Code Tool快速包装核心算法函数,投入仿真验证算法逻辑。如果发现性能瓶颈或需要更精细的控制,再针对性地将关键部分改写成手写的、高度优化的S-Function。切忌一开始就追求“最完美”的方案,快速迭代验证往往更重要。

3. 手把手实战:两种主流集成方法详解

理论说得再多,不如动手做一遍。我们以集成一个简单的遗留C函数为例,演示最实用的两种方法:使用Legacy Code Tool(快速入门)和手写C MEX S-Function(深入掌控)。

假设我们有一个遗留的C函数,用于计算移动平均滤波。它维护一个内部缓冲区,每次输入一个新值,返回当前缓冲区所有值的平均值。

moving_avg.h:

#ifndef MOVING_AVG_H #define MOVING_AVG_H void movingAvg_initialize(void); void movingAvg_update(double input, double *output); void movingAvg_terminate(void); #endif

moving_avg.c:

#include "moving_avg.h" #define BUFFER_SIZE 10 static double buffer[BUFFER_SIZE]; static int index = 0; static int isInitialized = 0; void movingAvg_initialize(void) { for(int i=0; i<BUFFER_SIZE; i++) { buffer[i] = 0.0; } index = 0; isInitialized = 1; } void movingAvg_update(double input, double *output) { if (!isInitialized) return; buffer[index] = input; index = (index + 1) % BUFFER_SIZE; double sum = 0.0; for(int i=0; i<BUFFER_SIZE; i++) { sum += buffer[i]; } *output = sum / BUFFER_SIZE; } void movingAvg_terminate(void) { isInitialized = 0; }

3.1 方法一:使用Legacy Code Tool(LCT)—— 自动化集成

LCT的思路是“描述”而非“编写”。你告诉MATLAB你的函数长什么样,它来帮你生成S-Function。

步骤1:创建并配置LCT对象在MATLAB命令窗口中,我们一步步配置:

% 1. 创建一个Legacy Code Tool对象 def = legacy_code('initialize'); % 2. 指定生成S-Function的名字 def.SFunctionName = 'sfun_moving_avg_lct'; % 3. 指定源文件和头文件 def.SourceFiles = {'moving_avg.c'}; def.HeaderFiles = {'moving_avg.h'}; % 4. 最关键的一步:描述函数的接口 % 格式:'返回值类型 函数名(参数类型1, 参数类型2, ...)' % 对于void函数,返回值类型写‘void’ % 参数类型:输入用‘double’(或其他Simulink支持的类型),输出用‘double*’(指针) def.OutputFcnSpec = 'void movingAvg_update(double u1, double y1[1])'; % 5. 指定初始化函数和终止函数(如果有) def.InitializeConditionsFcnSpec = 'void movingAvg_initialize()'; def.TerminateFcnSpec = 'void movingAvg_terminate()'; % 6. 设置采样时间(-1表示继承,即与驱动它的模块同速率) def.SampleTime = [-1, 0]; % [采样时间, 偏移量], -1表示继承 % 7. 选项:是否支持Simulink的“可变大小信号”(通常关闭) def.Options.supportVariableSizeSignals = false;

步骤2:生成、编译与验证配置完成后,使用LCT命令自动执行后续所有步骤:

% 生成S-Function的C源码和TLC文件 legacy_code('generate_for_sim', def); % 编译生成的C源码,生成MEX文件(Windows下是.sfx64文件) legacy_code('compile', def); % 生成一个用于测试的Simulink模型,并自动运行仿真,验证集成是否正确 legacy_code('slblock_generate', def);

执行完slblock_generate后,MATLAB会自动打开一个测试模型,里面已经放置好了刚生成的S-Function模块。运行仿真,如果一切正常,你就能在Scope里看到滤波后的信号。

注意事项OutputFcnSpec的字符串格式必须精确匹配,包括空格。y1[1]表示一个标量输出指针。对于多个输入输出,可以这样写:void myFunc(double u1, double u2, double y1[1], double y2[1])。LCT会自动处理从Simulink信号到C函数参数的映射。

步骤3:集成到你的主模型生成成功后,在你的Simulink库浏览器中(可能需要刷新),Simulink/User-Defined Functions下会出现一个以sfun_moving_avg_lct命名的模块。你可以像拖拽任何标准模块一样,把它拖到你的模型中使用了。

3.2 方法二:手写C MEX S-Function —— 完全掌控

当LCT无法满足你的复杂需求时(例如,需要处理复杂数据结构、自定义内存管理、或实现多速率功能),就需要手写S-Function。我们来实现一个与LCT功能等价的手写版本。

步骤1:创建S-Function模板文件创建一个名为sfun_moving_avg_manual.c的文件。一个最基础的S-Function需要实现以下几个核心回调函数:

  • mdlInitializeSizes: 定义模块的输入/输出端口数量、数据类型、采样时间等基本信息。
  • mdlInitializeSampleTimes: 定义模块的采样时间。
  • mdlStart: 执行一次性的初始化操作(分配内存、调用遗留代码的初始化函数)。
  • mdlOutputs: 在每个采样步长中,计算模块的输出(这里是调用遗留的movingAvg_update函数)。
  • mdlTerminate: 仿真结束时,执行清理操作(调用遗留代码的终止函数)。

步骤2:编写S-Function代码

#define S_FUNCTION_NAME sfun_moving_avg_manual #define S_FUNCTION_LEVEL 2 #include "simstruc.h" // Simulink数据结构头文件 #include "moving_avg.h" // 我们的遗留代码头文件 /*================* * S-function方法 * *================*/ /* 函数: mdlInitializeSizes * 作用:定义S-Function的基本特性 */ static void mdlInitializeSizes(SimStruct *S) { // 设置动态调整大小的参数数量为0(我们不需要可调参数) ssSetNumSFcnParams(S, 0); if (ssGetNumSFcnParams(S) != ssGetSFcnParamsCount(S)) { return; /* 参数不匹配,Simulink会报错 */ } // 设置输入端口数量为1 if (!ssSetNumInputPorts(S, 1)) return; // 配置第一个输入端口:标量,双精度浮点数 ssSetInputPortWidth(S, 0, 1); ssSetInputPortDataType(S, 0, SS_DOUBLE); ssSetInputPortDirectFeedThrough(S, 0, 1); // 输入是否直接影响输出?是。 // 设置输出端口数量为1 if (!ssSetNumOutputPorts(S, 1)) return; // 配置第一个输出端口:标量,双精度浮点数 ssSetOutputPortWidth(S, 0, 1); ssSetOutputPortDataType(S, 0, SS_DOUBLE); // 设置工作向量的数量(这里不需要DWork,用遗留代码自己的静态变量) ssSetNumContStates(S, 0); ssSetNumDiscStates(S, 0); // 设置采样时间:继承驱动模块的采样时间 ssSetNumSampleTimes(S, 1); // 指定此S-Function可以用于代码生成(TLC文件需要另写) ssSetOptions(S, SS_OPTION_EXCEPTION_FREE_CODE); } /* 函数: mdlInitializeSampleTimes * 作用:设置采样时间 */ static void mdlInitializeSampleTimes(SimStruct *S) { // 设置采样时间为继承(-1),偏移量为0 ssSetSampleTime(S, 0, INHERITED_SAMPLE_TIME); ssSetOffsetTime(S, 0, 0.0); } /* 函数: mdlStart * 作用:仿真开始时调用一次,用于初始化 */ #define MDL_START static void mdlStart(SimStruct *S) { // 调用遗留代码的初始化函数 movingAvg_initialize(); } /* 函数: mdlOutputs * 作用:在每个采样时刻计算输出 */ static void mdlOutputs(SimStruct *S, int_T tid) { // 获取输入和输出信号指针 InputRealPtrsType uPtrs = ssGetInputPortRealSignalPtrs(S, 0); real_T *y = ssGetOutputPortRealSignal(S, 0); double input = *uPtrs[0]; // 解引用获取输入值 double output; // 调用遗留代码的核心函数 movingAvg_update(input, &output); y[0] = (real_T)output; // 将结果赋给输出端口 } /* 函数: mdlTerminate * 作用:仿真结束时调用,用于清理 */ static void mdlTerminate(SimStruct *S) { // 调用遗留代码的终止函数 movingAvg_terminate(); } /* 以下宏是必需的,用于将上述函数与Simulink引擎关联起来 */ #ifdef MATLAB_MEX_FILE /* 判断是否被编译为MEX文件 */ #include "simulink.c" #else #include "cg_sfun.h" #endif

步骤3:编译与使用在MATLAB命令行中,导航到C文件所在目录,使用mex命令编译:

mex sfun_moving_avg_manual.c moving_avg.c -I.

-I.表示将当前目录加入头文件搜索路径。编译成功后,会生成一个MEX文件(如sfun_moving_avg_manual.mexw64)。

在Simulink中,从库浏览器找到User-Defined Functions下的S-Function模块,拖入模型。双击模块,在S-function name框中填入sfun_moving_avg_manual,点击OK。连接输入输出,即可使用。

实操心得:手写S-Function的关键点

  • ssSetInputPortDirectFeedThrough:这个标志至关重要。如果设为1(真),表示输出直接依赖于当前时刻的输入。对于我们的移动平均滤波器,这是错误的!因为输出是过去10个输入的平均值,不依赖于当前瞬时输入。正确的应设为0。这会影响Simulink求解器的代数环检测和排序。这是一个非常常见的错误设置。
  • 数据类型一致性:Simulink中的real_T通常对应C中的double。确保遗留代码的数据类型与Simulink端口定义匹配。
  • 内存管理:如果遗留代码需要动态分配内存,应在mdlStart中分配,在mdlTerminate中释放。对于有状态的代码,可以使用ssGetDWork来分配持久化存储空间,这比使用静态变量更安全,尤其是在模型引用或快速重启仿真时。

4. 进阶议题与代码生成

将遗留代码成功集成到仿真中只是第一步。对于嵌入式项目,最终目标是生成高效、可靠的产品代码。这就涉及到TLC(Target Language Compiler)文件和与Embedded Coder的配合。

4.1 为代码生成准备S-Function:TLC文件

TLC文件告诉Simulink Coder/Embedded Coder如何将你的S-Function模块转换成目标代码。没有TLC文件,你的S-Function在仿真时一切正常,但一旦点击“生成代码”,就会报错。

对于通过Legacy Code Tool生成的S-Function,它会自动生成一个对应的TLC文件(sfun_moving_avg_lct.tlc)。这个文件通常是够用的,因为它基于你提供的OutputFcnSpec等规范。

对于手写的S-Function,你需要自己编写TLC文件。一个最基本的TLC文件如下 (sfun_moving_avg_manual.tlc):

%% 文件: sfun_moving_avg_manual.tlc %% 为手写S-Function生成代码 %implements "sfun_moving_avg_manual" "C" %% 函数: BlockInstanceSetup %% 作用:为生成的代码中的这个模块实例进行设置 %% %function BlockInstanceSetup(block, system) void %assign rollVars = ["U", "Y"] %% 声明输入输出变量 %<LibBlockInputSignal(0, "", rollVars, 0)> /* 声明输入变量 */ %<LibBlockOutputSignal(0, "", rollVars, 0)> /* 声明输出变量 */ %endfunction %% 函数: Outputs %% 作用:生成模块输出计算部分的代码 %% %function Outputs(block, system) Output /* 获取输入输出变量名 */ %assign u = LibBlockInputSignal(0, "", rollVars, 0) %assign y = LibBlockOutputSignal(0, "", rollVars, 0) /* 调用遗留代码函数 */ %<y> = movingAvg_update(%<u>); %endfunction

这个TLC文件做了两件事:1. 在BlockInstanceSetup中声明了模块的输入输出变量。2. 在Outputs函数中,生成了调用我们遗留函数movingAvg_update的C代码。

关键点:TLC文件的语法是另一门“语言”。对于简单集成,模仿LCT生成的TLC或Simulink自带的例子是最快的学习方式。复杂情况(如需要生成结构体、调用外部库)需要深入学习TLC编程。

4.2 与Embedded Coder的集成

当使用Embedded Coder生成生产代码时,你还需要考虑更多工程化细节:

  1. 数据存储类(Storage Class):你需要指定S-Function内部状态(如我们的缓冲区buffer和索引index)在生成代码中的存储方式。是通过DWork(默认在生成的源文件中定义为静态变量)?还是需要映射到特定的内存地址(如使用Simulink.Parameter对象并指定CustomStorageClass)?这通常在S-Function的mdlInitializeSizes中通过ssSetDWork相关函数,并结合模型数据字典来配置。
  2. 代码效率:生成的代码中,对S-Function的调用是直接的函数调用。要确保你的遗留代码本身是高效的。避免在mdlOutputs中调用malloc/free
  3. 多实例支持:如果你的模型中有多个相同的S-Function模块,它们默认会共享静态变量,这会导致冲突。为了实现真正的可重入(多实例),必须使用ssGetDWork来为每个模块实例分配独立的状态存储空间,而不是使用C文件中的静态变量。
  4. 验证生成代码:使用Embedded Coder的“代码接口报告”和“代码跟踪”功能,检查生成的代码是否正确地调用了你的遗留函数,数据流是否符合预期。

实操心得:对于生产代码生成,强烈建议优先使用Legacy Code Tool。它不仅生成了S-Function,还生成了基本可用的TLC文件,并且其生成模式更符合Embedded Coder的规范。手写方案虽然灵活,但需要你自行保证TLC文件的正确性和生成代码的质量,调试起来更复杂。

5. 避坑指南与调试技巧

集成过程很少一帆风顺。下面是一些我踩过坑后总结出的常见问题与解决方法。

5.1 编译与链接问题

  • 错误:未找到编译器
    • 现象:运行mexlegacy_code('compile')时提示找不到C编译器。
    • 解决:运行mex -setup选择已安装的编译器(如MinGW-w64或Microsoft Visual C++)。确保MATLAB支持的编译器版本已正确安装。
  • 错误:未定义的外部符号
    • 现象:链接错误,提示movingAvg_update等函数未定义。
    • 解决:检查mex命令是否包含了所有必要的源文件(.c文件)。确保头文件路径正确(-I选项)。检查函数名拼写是否与头文件声明完全一致(C语言区分大小写)。
  • 错误:LNK2005: 符号已在...中定义
    • 现象:多个源文件定义了相同的全局变量(如我们的bufferindex)。
    • 解决:这是多实例支持问题的典型表现。必须将S-Function中的状态从C文件的静态变量迁移到Simulink的DWork向量中。这是手写S-Function进阶必须掌握的技能。

5.2 仿真运行时问题

  • 问题:仿真结果不正确或输出为NaN/Inf
    • 排查
      1. 检查直接馈通标志:这是最常见的原因。确认ssSetInputPortDirectFeedThrough设置是否正确。如果算法输出不依赖于当前输入,必须设为0
      2. 在遗留代码中添加调试输出:在C代码的关键位置使用printfmexPrintf打印中间变量值。编译时需确保MATLAB_MEX_FILE宏已定义,且包含mex.h
      3. 使用MATLAB调试器:对于MEX文件,可以在编译时加入-g调试标志,然后在Visual Studio等外部调试器中附加到MATLAB进程进行源码级调试。
      4. 验证数据类型:确保Simulink端口的数据类型(SS_DOUBLE,SS_INT32等)与C函数参数类型匹配。不匹配会导致内存解释错误。
  • 问题:仿真速度异常缓慢
    • 排查
      1. 检查采样时间:如果S-Function的采样时间设置不当(如设为连续或过小的固定步长),会导致被过度调用。
      2. 避免在mdlOutputs中调用重型初始化:初始化操作应放在mdlStart中。
      3. 检查遗留代码本身效率:可能是算法复杂度问题。尝试在Simulink外对遗留代码进行性能剖析。

5.3 代码生成问题

  • 问题:生成代码时失败,提示TLC错误
    • 排查:仔细检查TLC文件语法。最常见的错误是变量引用格式%<var>使用错误,或函数名拼写错误。对比Simulink自带示例的TLC文件。
  • 问题:生成的代码编译失败
    • 排查
      1. 检查生成代码的目录中,是否包含了所有必要的源文件(你的.c.h文件)。需要在Embedded Coder的配置中,将自定义源文件添加到“自定义代码”路径。
      2. 检查生成代码的编译环境(如Makefile)是否正确设置了包含路径和库路径。
      3. 确保遗留代码本身在目标编译器下是可编译的,没有使用仿真环境特有的库(如stdio.h中的某些函数在嵌入式环境中不可用)。

5.4 一个综合调试案例:直接馈通标志引发的代数环

现象:在一个包含反馈回路的模型中,集成了移动平均滤波S-Function后,仿真无法启动,报错“检测到代数环”。

分析:代数环发生在Simulink检测到一组模块的输出直接依赖于同一时刻的输入,且形成闭环。我们的移动平均滤波器输出y(t)依赖于过去10个输入[u(t-1), u(t-2), ..., u(t-10)],而不依赖于u(t)。因此,它不是直接馈通系统。

根本原因:在手写S-Function的mdlInitializeSizes中,错误地将ssSetInputPortDirectFeedThrough(S, 0, 1)设为了1。这欺骗了Simulink,让它以为该模块的输出y(t)依赖于输入u(t)。当这个模块被放在一个反馈回路中时,Simulink就认为形成了一个“u(t) -> S-Function -> y(t) -> ... -> u(t)”的瞬时依赖环,即代数环。

解决:将标志位改为0ssSetInputPortDirectFeedThrough(S, 0, 0);。重新编译S-Function,仿真即可正常运行。

这个案例深刻说明,理解Simulink仿真机制(如直接馈通、代数环、采样时间)对于正确集成遗留代码至关重要。不能仅仅满足于“代码能跑”,更要理解其“为什么”能跑。