☰
深入理解 ARM SCP Firmware 的 Event 与 Notification 机制
2026/9/27 11:47:36 网站建设 项目流程

SCP Firmware 负责系统中的电源域、时钟、传感器、通信接口等管理工作。为了控制复杂度,这些功能被拆分成相互独立的 Module。Module 之间既需要协作,又不应该彼此依赖具体实现,因此 Framework 提供了几种受控的交互方式,其中最重要的异步通信机制就是Event和Notification。

本文面向第一次接触 SCP Firmware 的读者,从整体运行模型出发,逐步介绍 Event 和 Notification 的概念、数据结构、请求与响应机制,并结合 Sensor、Power Domain 和 Clock Module 的实际代码说明它们如何工作。

本文讨论的是 SCP Firmware 内部 Module 之间的通信,不是 SCP 与 AP 之间通过 MHU、SCMI 等协议进行的跨处理器通信。

1. 先建立整体认识

可以先用两句话概括二者:

  • Event 是点对点的异步消息:发送者知道接收者是谁,希望接收者执行一项工作,并且可以要求响应。
  • Notification 是发布/订阅消息:发布者只声明“某件事发生了”,Framework 将消息发送给所有提前订阅它的对象。

Arm® System Control Processor (SCP) Firmware-101

Notification 并不是一套独立于 Event 的底层设施。在 Framework 内部,Notification 本质上是一种特殊 Event:它同样使用struct fwk_event,进入相同的事件队列,也支持普通响应和延迟响应。二者最主要的区别,是消息的寻址方式和所表达的业务语义。

2. Event 背后的运行模型

理解 Event 之前,需要先了解 SCP Firmware 的主循环。默认裸机运行模式下,它的核心逻辑可以简化为:

for(;;){fwk_process_event_queue();if(fwk_log_unbuffer()==FWK_SUCCESS){fwk_arch_suspend();}}

主循环不断处理队列里的消息;当事件和缓存日志都处理完后,处理器进入休眠,等待中断再次唤醒。

Framework 主要维护三类队列:

free_event_queue 空闲 Event 对象池 event_queue 等待处理的普通 Event isr_event_queue 中断上下文产生的 Event

调用fwk_put_event()时,Framework 从预分配的对象池中取得一个 Event,将调用者提供的内容复制进去,再放入普通队列或 ISR 队列。发送函数返回时,接收者通常还没有开始处理消息。

主循环取出 Event 后,根据target_id找到目标 Module,然后根据is_notification选择回调:

process=event->is_notification?module->process_notification:module->process_event;

因此,默认情况下 SCP Firmware 是一种run-to-completion的协作式事件模型:一个普通事件回调开始执行后,会一直运行到返回,随后主循环才处理下一个 Event。它不是带线程、时间片和任务优先级的 RTOS 调度器。

这带来一个直接要求:process_event()和process_notification()不应长时间阻塞。耗时操作应交给硬件、中断或后续 Event 完成。

3. Event:点对点的异步消息

3.1 Event 中保存了什么

struct fwk_event的核心字段可以简化为:

structfwk_event{fwk_id_tsource_id;fwk_id_ttarget_id;uint32_tcookie;bool is_response;bool response_requested;bool is_notification;bool is_delayed_response;fwk_id_tid;uint8_tparams[FWK_EVENT_PARAMETERS_SIZE];};

各字段的作用如下:

字段作用
source_id消息发送者,可以是 Module、Element 或 Sub-element
target_id消息接收者
idEvent 类型,由拥有该 Event 的 Module 定义
params请求或响应参数,当前固定为 16 字节
response_requested发送者是否要求响应
is_response当前消息是否是另一个 Event 的响应
is_delayed_response响应是否需要推迟发送
cookieFramework 分配的关联标识,用于匹配请求和延迟响应

3.2 Event ID 属于接收方

普通请求 Event 有一条非常重要的规则:

Event ID 由接收该请求的 Module 定义。

假设 Module A 向 Module B 发送请求,那么event.id应当是 Module B 公开或内部定义的 Event ID,target_id也必须属于 Module B。它表达的语义是:“A 请求 B 执行一种由 B 定义的操作”。

一个典型的发送过程如下:

structfwk_eventrequest={.target_id=target_id,.id=target_event_id,.response_requested=true,};structrequest_params*params=(structrequest_params*)request.params;params->value=value;status=fwk_put_event(&request);

如果代码正在处理另一个 Event,Framework 会把当前 Event 的目标实体作为新 Event 的source_id。如果在启动阶段或中断上下文等位置发送消息,则通常需要显式提供合法的source_id。

由于 Framework 会复制 Event,像上面这样使用栈变量是安全的。不过,params中如果保存了指针,指针所指向对象的生命周期仍然必须覆盖整个异步处理过程。

3.3 接收 Event

能够接收 Event 的 Module 需要在描述符中提供process_event:

staticintmodule_process_event(conststructfwk_event*event,structfwk_event*resp_event){switch(fwk_id_get_event_idx(event->id)){caseMODULE_EVENT_IDX_REQUEST:returnprocess_request(event,resp_event);default:returnFWK_E_PARAM;}}conststructfwk_modulemodule_example={.event_count=MODULE_EVENT_IDX_COUNT,.process_event=module_process_event,};

process_event接口参数:event是收到的消息;当请求方要求响应时,resp_event是 Framework 准备好的响应对象,接收方主要负责填写响应参数。

3.4 三种响应方式

Event 支持三种常见处理方式。

不需要响应

发送方保持:

.response_requested=false

接收方处理完 Event 后,本次交互结束。它适合“触发一次动作,但发送者不关心结果”的场景。

标准响应

发送方设置:

.response_requested=true

Framework 会预先构造resp_event,将请求的source_id和target_id对调。接收方填写响应参数并返回后,Framework 自动设置is_response = true,再把响应放回事件队列。

完整过程是:

Module A Framework Module B | | | |--- request -------->| | | |--- process_event -->| | |<-- fill response ---| | | | |<-- response --------| |

响应仍是异步消息,不会在 Module B 返回时直接同步调用 Module A。Module A 最终也在自己的process_event()中收到它,并通过event->is_response判断消息方向。

延迟响应

如果接收方暂时得不到结果,可以在处理请求时设置:

resp_event->is_delayed_response=true;

Framework 此时不会立即发送响应,而是把响应对象保存在目标实体的 delayed-response 列表中。等硬件操作或其他异步过程完成后,Module 可以通过cookie找回响应对象,填写结果,再调用fwk_put_event()发回请求者。

延迟响应适用于传感器采样、I2C 传输、电源状态切换等无法立即完成的操作。

3.5 Light Event

Framework 还提供struct fwk_event_light。它只保留source_id、target_id、id和response_requested,不包含参数区和延迟响应所需字段。

Light Event 适合不需要携带数据、但对构造开销敏感的路径,例如部分 DVFS 场景。它不能用于 Notification,也不能用于 delayed response。

4. Notification:一对多的发布/订阅

Event 要求发送者明确填写target_id。但有些消息并不是“请某个对象执行操作”,而是“我的状态发生了变化,感兴趣的对象可以处理”。这正是 Notification 的用途。

Notification 的完整生命周期包括两个阶段:订阅和发布。

4.1 订阅

订阅者调用:

status=fwk_notification_subscribe(notification_id,source_id,target_id);

三个参数分别表示:

  • notification_id:希望接收哪一种通知;
  • source_id:只接收哪个 Module 或 Element 发出的通知;
  • target_id:通知到达后,交给订阅方的哪个实体处理。

订阅通常在 Module 的start()阶段完成,此时各 Module 已完成初始化和绑定。

4.2 Notification ID 属于发布方

Notification 与普通 Event 在 ID 所有权上正好相反:

Notification ID 由发布通知的源 Module 定义。

例如,Power Domain Module 定义“电源状态已经改变”通知。Clock Module 可以订阅它,但通知 ID 仍然属于 Power Domain,因为这条消息描述的是 Power Domain 自身的状态。

4.3 发布

发布者构造一个struct fwk_event,但不需要逐个指定接收者:

structfwk_eventnotification={.source_id=source_id,.id=notification_id,.response_requested=false,};structnotification_params*params=(structnotification_params*)notification.params;params->new_state=new_state;status=fwk_notification_notify(&notification,&subscriber_count);

Framework 根据notification_id + source_id查询订阅表,为每个订阅者生成一份消息副本,填写不同的target_id,然后逐一放入事件队列。

subscriber_count返回成功发送的通知数量。即使没有订阅者,发布操作也可以正常完成,此时计数为 0。

4.4 接收 Notification

订阅方通过process_notification()接收:

staticintmodule_process_notification(conststructfwk_event*event,structfwk_event*resp_event){if(event->is_response){returnprocess_notification_response(event);}if(fwk_id_is_equal(event->id,expected_notification_id)){returnprocess_state_change(event,resp_event);}returnFWK_E_HANDLER;}

Notification 同样可以设置response_requested = true。这时每个订阅者都会产生独立响应,发布者可以结合subscriber_count统计是否已收到全部响应。

这类机制特别适合构建依赖链。例如系统进入低功耗状态之前,Power Domain 先发出 pre-transition Notification;Clock、内存控制器或唤醒模块完成准备后分别响应;发布者收到所有响应后,才继续真正的状态切换。

Notification 是可选构建特性,需要启用SCP_ENABLE_NOTIFICATIONS,相关代码通常由BUILD_HAS_NOTIFICATION条件编译保护。

5. Event 与 Notification 的区别

对比项EventNotification
通信模型点对点发布/订阅,一对多
接收者发送时明确指定由 Framework 查询订阅表决定
ID 所属方接收方 Module发布方 Module
处理入口process_event()process_notification()
典型语义请求执行操作、推进状态机宣布状态或事实发生变化
响应可选,通常只有一个响应方可选,每个订阅者分别响应
底层承载Event 队列同一个 Event 队列
耦合程度发送方知道目标发布方不需要知道订阅者

选择时可以使用一个简单判断:

  • 如果消息表达“请你完成某件事”,并且目标明确,使用 Event;
  • 如果消息表达“某件事已经或即将发生”,可能有零个、一个或多个关注者,使用 Notification。

6. Event 实例:Sensor 的异步读取

Sensor Module 展示了 Event 与 delayed response 如何配合完成一次异步操作。阅读代码前,先区分参与流程的三个角色:

  • Sensor Client:传感器数据的使用者,例如 Thermal Management 或 SCMI Sensor Module;
  • Sensor HAL:module/sensor,向 Client 提供统一的 Sensor API,并协调异步请求;
  • Sensor Driver:面向具体硬件的驱动,例如 Juno 平台的 PVT 温度传感器驱动。

下面以一次传感器读取为例说明主流程。代码基于module/sensor/src/mod_sensor.c,省略了错误处理和并发控制。

6.1 Client 通过 API 发起读取

Client 通常在自己的process_event()中调用 Sensor HAL 的get_data()。Sensor HAL 随后调用具体 Driver 的get_value():

status=ctx->driver_api->get_value(ctx->config->driver_id,&ctx->last_read.value);

如果数据可以立即取得,Driver 返回FWK_SUCCESS,调用过程同步结束。对于需要启动硬件采样并等待中断的设备,Driver 返回FWK_PENDING。这表示请求已被接受,但结果稍后才能提供。

Sensor HAL 随后提交一个要求响应的READ_REQUESTEvent,并向 Client 返回FWK_PENDING:

structfwk_eventrequest={.target_id=sensor_id,.id=mod_sensor_event_id_read_request,.response_requested=true,};status=fwk_put_event(&request);returnFWK_PENDING;

虽然这个 Event 由 Sensor HAL 构造,并且目标也是 Sensor HAL 的 Sensor Element,但它并不是普通的“自己发给自己”。fwk_put_event()会根据当前正在处理的 Event 自动补充source_id,因此其逻辑方向是:

Client -> Sensor HAL

这里的READ_REQUEST也不是让 Driver 再读取一次硬件。Driver 侧的读取流程已经由前面的get_value()发起;这个 Event 的主要作用是为异步结果建立一条返回 Client 的响应路径。

6.2 Sensor HAL 延迟响应

Framework 发现response_requested为true后,会为该请求准备一个响应 Event,并自动交换通信双方:

请求:Client -> Sensor HAL 响应:Sensor HAL -> Client

当主循环把READ_REQUEST交给sensor_process_event()时,硬件采样还没有完成,因此 Sensor HAL 不立即发送这个响应,而是将其标记为 delayed response:

caseSENSOR_EVENT_IDX_READ_REQUEST:ctx->cookie=event->cookie;resp_event->is_delayed_response=true;returnFWK_SUCCESS;

Framework 随后保存的是已经准备好的“Sensor HAL 到 Client”的响应 Event,而不是原始的请求 Event。cookie用于在读取完成后找到与本次请求对应的响应。

6.3 Driver 通知读取完成

硬件采样完成后,具体 Driver 通常在处理中断或驱动内部 Event 时取得结果,然后通过 Sensor HAL 提供的reading_complete()回调上报数据。

reading_complete()保存读取结果,并向 Sensor HAL 对应的 Sensor Element 提交一个READ_COMPLETEEvent。这个 Event 的方向是:

Sensor Driver -> Sensor HAL

因此,READ_COMPLETE只是 Driver 与 Sensor HAL 之间的内部完成事件,并不会直接发送给 Client。

6.4 Sensor HAL 将结果返回 Client

Sensor HAL 处理READ_COMPLETE时,使用先前保存的cookie取回 delayed response,将读取结果写入 Client 提供的数据对象,然后提交该响应:

caseSENSOR_EVENT_IDX_READ_COMPLETE:status=fwk_get_delayed_response(event->target_id,ctx->cookie,&read_response);structmod_sensor_event_params*response_params=(void*)read_response.params;sensor_data_copy(response_params->sensor_data,&ctx->last_read);status=fwk_put_event(&read_response);

最终,Client 在自己的process_event()中收到来自 Sensor HAL 的响应,并继续处理传感器数据。完整流程如下:

Client Sensor HAL Sensor Driver | | | | get_data() | | |------------------------->| get_value() | | |------------------------------>| | | FWK_PENDING | | FWK_PENDING |<------------------------------| |<-------------------------| | | | | | READ_REQUEST | | |------------------------->| 保存 delayed response | | | | | | ...硬件采样 / 中断处理... | | | | | | reading_complete() | | | + READ_COMPLETE Event | | |<------------------------------| | delayed response | | |<-------------------------| |

需要注意,Client 最终收到的是READ_REQUEST的响应,而不是READ_COMPLETE。Framework 沿用原请求的 Event ID,并通过is_response标记它是一个响应。

实际实现还允许多个 Client 等待同一次硬件读取。第一个请求触发采样,后续请求先进入等待状态;读取完成后,process_pending_requests()使用同一份最新数据依次完成这些请求,避免重复启动硬件采样。

7. Notification 实例:Power Domain 通知 Clock

Power Domain 和 Clock 的协作展示了典型的一对多状态传播。以下代码同样省略了上下文获取、条件编译和错误分支,只保留订阅与通知的主线。

Clock Element 在启动阶段订阅 Power Domain 的状态变化:

status=fwk_notification_subscribe(pd_transition_notification_id,pd_source_id,clock_element_id);

当 Power Domain 完成状态切换后,它发布状态变化 Notification:

structfwk_eventnotification_event={.id=mod_pd_notification_id_power_state_transition,.response_requested=true,.source_id=FWK_ID_NONE};structmod_pd_power_state_transition_notification_params*params;params->state=new_state;status=fwk_notification_notify(&notification_event,&pd->power_state_transition_notification_ctx.pending_responses);

mod_pd_notification_id_power_state_transition == pd_transition_notification_id

if(fwk_id_is_equal(event->id,pd_transition_notification_id)){returnclock_process_pd_transition_notification(ctx,event);}

Clock 随后还可以继续发布自己的state_changedNotification,让依赖该时钟的其他 Module 感知变化。这样便形成:

Power Domain | | power-state-transition Notification v Clock | | clock-state-changed Notification v 其他订阅者

Power Domain 并不需要知道哪些 Clock 或其他 Module 关注它,Clock 也不需要写死后续消费者。这正是 Notification 降低模块耦合的价值。

8. 常见误区与使用建议

8.1 不要混淆 ID 的归属

这是最常见的错误:

  • 请求 Event 的 ID 属于目标 Module;
  • Notification 的 ID 属于源 Module。

Framework 的调试构建会检查 ID 与 source/target 的 Module 索引是否匹配。

8.2fwk_put_event()成功不等于业务完成

它通常只表示 Event 已成功复制并进入队列。真正的处理结果需要通过响应 Event、Notification 响应或后续状态查询获得。

8.3 注意参数区大小和对象生命周期

params只有FWK_EVENT_PARAMETERS_SIZE,当前为 16 字节。参数结构必须保证能够放入其中,最好在编译期检查大小。

栈上的 Event 可以安全发送,因为 Framework 会复制它;但参数中的裸指针不会自动复制其指向的数据,异步处理时尤其要注意悬空指针问题。

8.4 Handler 应尽快返回

默认事件处理是串行的。一个 Handler 长时间忙等,会阻塞后续 Event、Notification 和日志输出。耗时操作应拆成“启动操作”和“完成事件”两个阶段。

8.5 Event 池是有限资源

Framework 使用预分配的 Event 对象池。若生产速度长期高于消费速度,fwk_put_event()可能返回FWK_E_NOMEM。发送方必须检查返回值,设计时也应避免无界地生成 Event。

8.6 ISR 中只做必要工作

ISR 可以产生 Event,Framework 会先将其放入isr_event_queue,再交给普通事件上下文处理。推荐在 ISR 中完成清中断、读取必要状态和投递 Event,把复杂逻辑留给主循环。

8.7 Notification 发布者不要依赖固定订阅者

fwk_notification_notify()返回的数量可能是 0。除非业务协议明确要求至少有一个订阅者,否则发布方不应把“无人订阅”当作异常。

9. 源码阅读路径

建议按照以下顺序阅读源码:

  1. framework/include/fwk_event.h:Event 数据结构;
  2. framework/include/fwk_core.h:fwk_put_event()等接口;
  3. framework/src/fwk_core.c:事件池、队列、分发和响应;
  4. framework/include/fwk_notification.h:订阅与发布接口;
  5. framework/src/fwk_notification.c:订阅表和一对多投递;
  6. module/sensor/src/mod_sensor.c:delayed response 实例;
  7. module/power_domain/src/mod_power_domain.c与module/clock/src/mod_clock.c:Notification 依赖链实例。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询