3. 状态与转移语义
Stateflow 的语义核心不是“画了几个框”,而是“同一时刻哪些状态是活的、什么时候离开、离开和进入时按什么顺序执行动作”。如果这层语义理解错,模型可能语法通过、仿真也能跑,但行为与需求不一致,而且问题往往只在边界条件或并发路径上暴露。本节把互斥/并行、默认转移、条件转移以及动作执行顺序放在一起讲,因为它们共同决定一次状态切换的完整因果链。
一、互斥状态与并行状态:决定“同时有几个活状态”
互斥(OR)状态是最常见的组织方式:父状态下的子状态同一时刻只有一个处于激活状态。例如一个“电机控制”父状态包含 Idle、Run、Fault 三个子状态,任一时刻只有一个为活。互斥状态之间的切换必须由转移显式指定,不能同时进入两个兄弟状态。
并行(AND)状态则相反:父状态一旦被激活,其下所有并行子区域同时激活,各自独立演化。并行状态适合表达“同时成立的多条逻辑”,例如一个“系统运行”父状态同时包含“温度监控”和“通信监控”两个并行区域,温度区域在 Normal/Overheat 之间切换,通信区域在 Online/Offline 之间切换,两者互不阻塞。判断该用哪种,可以问一句:这些子状态是“二选一”还是“同时都要成立”?前者用互斥,后者用并行。
并行区域内部仍然是互斥的,即每个并行区域自己再按互斥方式组织子状态。并行父状态退出时,所有并行区域一起退出;这一点在写退出动作和复位逻辑时必须考虑,否则容易出现某个区域已复位、另一个区域还保留旧值的中间态。
二、默认转移:父状态首次激活时的入口
默认转移(default transition)决定父状态第一次被激活时进入哪个子状态。它通常画成从父状态边界指向某个子状态的短箭头,并带一个默认转移标签。默认转移只在父状态“首次进入”时生效,之后父状态内部的状态切换由普通转移控制,不会反复走默认转移。
默认转移可以带条件,但必须保证在父状态首次激活时至少有一条默认转移的条件为真,否则会报“无有效默认转移”的语义错误。实践中建议:给默认转移一个无条件分支作为兜底,其余带条件分支按优先级从上到下排列。例如父状态 MotorCtrl 的默认转移可写成:第一条 [faultFlag] 指向 Fault,第二条无条件指向 Idle。这样上电时若故障标志已置位,直接进入故障态,否则进入空闲态。
三、条件转移:条件、转移动作与优先级
条件转移的标签格式为 [条件]{转移动作},两者都可省略。条件为真且源状态处于激活状态时,转移被触发。若同一源状态有多条出边同时满足条件,Stateflow 按图形上的优先级(通常是从上到下、从左到右)选择第一条可执行的转移,因此条件之间应尽量互斥,或显式用优先级表达意图。
转移动作在转移被选中、源状态退出之后、目标状态进入之前执行。它适合做与“这次切换”强相关的赋值,例如 {mode = 2; faultCnt = 0;}。不要把长期维护的逻辑塞进转移动作,否则状态一多就难以追踪。
四、动作执行顺序:一次转移的完整时序
这是最容易出错的部分。一次状态切换的动作顺序为:源状态 exit 动作 → 转移动作 → 目标状态 entry 动作。若源状态和目标状态有共同父状态,父状态不会被退出和重新进入,因此父状态的 entry/exit 不会重复执行;只有跨出父状态边界时,父状态的 exit 才会执行。
并行状态下,多个区域的动作按区域在图形中的顺序依次执行,而不是真正并行求值。因此如果两个区域都写同一个变量,结果依赖执行顺序,应避免这种共享写。状态动作还包括 during(状态激活期间每个仿真步执行)和 on event(指定事件发生时执行),它们与 entry/exit 共同构成状态的生命周期动作。
示例:一次 Run → Fault 切换的动作顺序
Run 状态:
entry: runFlag = 1;
during: speed = speed + 1;
exit: runFlag = 0;
转移 Run → Fault:
条件: [faultFlag == 1]
动作: {faultCnt = faultCnt + 1;}
Fault 状态:
entry: pwmEnable = 0;
during: faultTimer = faultTimer + Ts;
执行顺序(faultFlag 由 0 变 1 的那一步):
1) Run.exit → runFlag = 0
2) 转移动作 → faultCnt = faultCnt + 1
3) Fault.entry → pwmEnable = 0
之后每步执行 Fault.during → faultTimer 累加
验证顺序是否正确,可以在 entry/exit/转移动作中写入不同的记录变量,仿真后用 Scope 或 Simulation Data Inspector 观察同一时刻的取值先后。例如把 runFlag、faultCnt、pwmEnable 一起记录,若看到 runFlag 先归零、faultCnt 再自增、pwmEnable 最后归零,说明顺序符合预期。若 pwmEnable 在 runFlag 之前变化,则说明状态层次或转移画法有误,需要检查是否误把动作写在了父状态或错误的边上。
常见语义陷阱:① 并行区域共享变量写入,结果依赖区域顺序;② 默认转移条件在首次激活时全为假,导致语义报错;③ 把本应放在转移动作里的“本次切换专属赋值”写进 during,造成每步重复执行;④ 误以为父状态每次子状态切换都会重新 entry,实际只有跨出父状态边界才会。
把以上四点串起来看:互斥/并行决定“同时有几个活状态”,默认转移决定“首次从哪进”,条件转移决定“何时走哪条边”,动作顺序决定“走的时候先做什么后做什么”。四者共同构成 Stateflow 状态逻辑的确定性语义,也是后续做覆盖分析和形式验证的基础。