4. 事件驱动建模基础
Stateflow 的执行语义本质上由“事件”驱动。理解事件从哪里来、如何进入状态图、在什么时刻被处理,以及事件与条件动作如何配合,是写出可预测状态逻辑的前提。本节围绕输入事件、本地事件、广播事件、事件队列,以及事件触发与条件动作的配合展开,重点说明它们在仿真与代码生成中的实际行为差异。
一、三类事件的作用域与来源
Stateflow 中的事件按作用域可分为输入事件、本地事件和输出事件,其中广播事件是本地事件的一种典型使用方式。输入事件由 Simulink 模型中的信号或函数调用触发,是状态图与外部环境交互的入口;本地事件在状态图内部定义和广播,用于在状态图内部传递触发信号;输出事件则用于向外部输出触发信号。三者的关键区别在于“谁产生、谁可见、何时被处理”。
输入事件通常绑定到 Simulink 的输入端口或函数调用输入。例如,一个车门控制状态图可以定义输入事件 E_Open 和 E_Close,分别由外部按键信号触发。本地事件则在 Stateflow 内部通过 send() 广播,例如在进入某个状态时广播 E_Timeout,让其他并行状态响应。广播事件的特点是“发出后进入队列,等待当前动作执行完毕后再被处理”,而不是立即打断当前执行。
二、事件队列与处理时序
Stateflow 使用事件队列管理待处理事件。当事件被触发时,它不会立即打断当前正在执行的动作,而是进入队列尾部,等待当前动作完成后按先进先出顺序处理。这一机制保证了状态图执行的原子性:一个状态的动作序列一旦开始,就会完整执行到结束,期间不会被新事件打断。
例如,在 E_Open 触发的转移动作中执行 send(E_Timeout),E_Timeout 会进入队列,而不是立即触发超时转移。只有当当前转移动作全部执行完毕,Stateflow 才会从队列头部取出 E_Timeout 并处理。如果队列中同时存在多个事件,处理顺序就是它们进入队列的顺序。
三、事件触发与条件动作的配合
事件触发和条件动作是转移上的两个不同要素。事件触发决定“什么时候检查这条转移”,条件动作决定“检查通过后做什么”。一条完整的转移标签通常写作:
E_Open [speed == 0] / door = OPEN; send(E_Timeout);
其中 E_Open 是触发事件,[speed == 0] 是条件,door = OPEN; send(E_Timeout); 是条件动作。执行逻辑是:当 E_Open 被处理时,先判断条件 speed == 0 是否成立;若成立,则执行动作并完成转移;若不成立,则转移不发生,事件被消耗,不会重复检查。
这里有一个容易出错的点:条件不成立时,事件已经被消耗。如果希望条件在后续时刻再次判断,需要重新触发事件,或者改用条件动作中的 after、every 等时间逻辑。例如,若希望车门在速度降为 0 后自动打开,可以写成 [speed == 0] / door = OPEN; 配合周期触发,而不是依赖单次 E_Open。
四、广播事件与并行状态的协同
广播事件在并行状态协同中非常有用。假设状态图包含两个并行区域:一个负责车门锁控制,一个负责报警监测。当车门打开时,锁控制区域广播 E_DoorOpened,报警监测区域收到后判断是否需要触发报警。由于广播事件进入队列,两个并行区域的执行顺序由 Stateflow 的并行执行语义决定,但事件本身不会丢失。
需要注意的是,广播事件默认只在当前状态图内部可见。如果需要在多个状态图之间传递事件,应使用输出事件或 Simulink 函数调用。本地事件和广播事件适合状态图内部的解耦,输入事件适合外部触发,输出事件适合向外通知。
五、实践中的配置与观察要点
在 Stateflow 中定义事件时,需要明确事件名称、作用域和触发方式。输入事件在“Model Explorer”中添加到状态图的输入事件列表,并绑定到 Simulink 端口;本地事件直接在状态图内定义,通过 send() 广播。仿真时可以通过“Stateflow 调试器”观察事件队列、当前状态和转移执行情况。
一个实用的检查清单如下:
- 事件作用域是否正确:输入事件是否绑定到正确的 Simulink 端口或函数调用?
- 广播事件是否在状态图内部定义,是否使用了
send()而非直接赋值? - 转移标签中事件、条件、动作的顺序是否正确,条件不成立时事件是否被消耗?
- 是否存在事件队列堆积:高频事件是否会导致队列过长,是否需要合并或丢弃策略?
- 并行状态之间的事件传递是否依赖广播,是否存在竞态或顺序依赖?