1. SMC系统调用与PSCI协议
图片
当Linux想要关机或者休眠的时候,这涉及到整个系统电源状态的变化,为了安全性Linux内核没有权利去直接执行了,需要陷入到EL3等级去执行,可以参考之前文章ARM ATF入门-安全固件软件介绍和代码运行,在EL3中处理的程序是BL31,把SMC系统调用的参数转化为PSCI协议去执行,这时如果有SCP那A核就憋屈了,自己没权利执行需要通过SCMI协议上报给SCP了。这就是整个过程的软件协议栈如上图中:
- 用户层:首先用户发起的一些操作,通过用户空间的各service处理,会经过内核提供的sysfs,操作cpu hotplug、device pm、EAS、IPA等。
- 内核层:在linux内核中,EAS(energy aware scheduling)通过感知到当前的负载及相应的功耗,经过cpu idle、cpu dvfs及调度选择idle等级、cpu频率及大核或者小核上运行。IPA(intrlligent power allocation)经过与EAS的交互,做热相关的管理。
- ATF层:Linux kernel中发起的操作,会经过电源状态协调接口(Power State Coordination Interface,简称PSCI),由操作系统无关的framework(ARM Trusted Firmware,简称ATF)做相关的处理后,通过系统控制与管理接口(System Control and Management Interface,简称SCMI),向系统控制处理器(system control processor,简称SCP)发起低功耗操作。
- SCP层:SCP(系统控制处理器system control processor)最终会控制芯片上的sensor、clock、power domain、及板级的pmic做低功耗相关的处理。
总结:用户进程 --sysfs--> 内核(EAS、IPA)--PSCI--> ATF --SCMI-->SCP --LPI--> 功耗输出器件
1.1 SMC指令
上面看完有一个整体的认识,下面进入正题,先介绍下什么是SMC指令,为什么走SMC就是安全通道,Linux直接给SCP通信就是非安全通道,这两种通道怎么去区分?
首先看SMC规范,ARM官方文档地址:
https://developer.arm.com/documentation/den0028/latest
《DEN0028E_SMC_Calling_Convention_1.4》本文档定义了一种通用的调用机制,可与Armv7和Armv8架构中的安全监视器调用(SMC)和系统监控程序调用(HVC)指令一起使用。
SMC指令用于生成一个同步异常,该异常由运行在EL3中的安全监视器代码处理。参数和返回值将在寄存器中传递。在由安全监视器处理之后,由指令产生的调用可以传递到受信任的操作系统或安全软件堆栈中的其他实体。
HVC指令用于生成由在EL2中运行的管理程序处理的同步异常。参数和返回值将在寄存器中传递。管理程序还可以捕获由客户操作系统(在EL1)发出的SMC调用,这允许适当地模拟、传递或拒绝调用。
本规范旨在简化集成和减少软件层之间的碎片化,例如操作系统、系统管理程序、受信任的操作系统、安全监视器和系统固件。具体的各种定义可以自己看手册,我们在Linux代码中执行smc调用的时候的函数例如关机为:
#define PSCI_0_2_FN_BASE 0x84000000
#define PSCI_0_2_FN(n) (PSCI_0_2_FN_BASE + (n))
#define PSCI_0_2_FN_SYSTEM_OFF PSCI_0_2_FN(8)
static void psci_sys_poweroff(void)
{
invoke_psci_fn(PSCI_0_2_FN_SYSTEM_OFF, 0, 0, 0);
}
PSCI_0_2_FN_SYSTEM_OFF的值计算为:0x84000000+8,在规范的表6-2:分配给不同服务的功能标识符的子范围中,
图片
表中的各种功能就是走安全通道的,不是SMC或者HVC命令的功能就是非安全通道的,当然也可以根据自己的需求选择,一般PSCI协议中的功能都是走安全通道。
1.2 PSCI协议
PSCI协议官方地址:
https://developer.arm.com/documentation/den0022/d/
《Power_State_Coordination_Interface_PDD_v1_1_DEN0022D》
本文档定义了一个电源管理的标准接口,操作系统供应商可用于在ARM设备上使用不同特权级别的监控软件。该接口旨在在以下电源管理场景中代码通用化:
- 内核空闲管理。
- 动态添加和删除核心,以及辅助核心引导。
- 系统关闭和复位。
该接口不包括动态电压和频率缩放(DVFS)或设备电源管理(例如,对图形处理器等外设的管理)。
为什么需要PSCI?
具有电源管理感知的操作系统动态地改变核心的电源状态,平衡可用的计算容量以匹配当前的工作负载,同时努力使用最小的功率量。其中一些技术可以动态地打开和关闭内核,或将它们置于静止状态,在静止状态下它们不再执行计算。这意味着它们消耗的能量很少。这些技术的主要例子是:
- 空闲管理:当操作系统中的内核在核心上没有线程可以调度时,它会将该核心置于时钟门控、保留状态,甚至是完全电源门控状态。然而,该核心仍然可用于操作系统。
- 热插拔:当计算需求低时,核心会物理关闭,当需求增加时恢复在线。该操作系统将迁移所有远离离线的核心的中断和线程,并在它们重新联机时重新平衡负载。
具体包含那些功能,可以自己去看规范文档,这里截图算个记录:
图片
比如关机就是5.10里面的内容。
2. SCMI协议
现在继续聊SCP里面的东西,上来就是SCMI协议,同样还是去ARM官网找:
《DEN0056B_System_Control_and_Management_Interface_v2_0》
这个协议在哪里用到,我们来看一个图:
图片
SCP会以服务的方式来支持AP参与运行管理,这也就需要SCP和AP之间有一个通信接口。这个通信接口在硬件上可以通过共享存储和MHU(Message Handling Unit)实现;在软件上,通过定义一组通信协议来实现。
主要涉及的模块如下:
- mhu模块:Message Handling Unit (MHU)在module/mhu/src/mod_mhu.c中实现
- msg_smt模块:Shared Memory Transport 是一种用于描述系统内存拓扑的数据结构。在ARM 架构中,SCP 固件使用 Shared Memory Transport来提供有关系统内存的信息,如地址范围、类型、属性等。System Memory Tables 通常由系统固件在启动过程中生成,并由SCP 固件和其他系统组件使用。它们允许系统软件了解和管理系统中可用的内存资源。
- SCMI模块:System Control & Management Interface (SCMI)
- 业务处理模块,为scmi protocol模块例如scmi_power_domain
SCMI抽象出协议和传输两层,协议层描述能够支持的命令,传输层定义了命令通过什么方式传输,发送命令方称为agent。有个限制,每个agent的传输通道必须一个或者多个,然后如果有安全需求,那安全AP必须使用安全的通道进行传输数据。
图片
协议层:
- 通道(channel)必须是分开独立的,各个agent不能使用同一个。避免platform无法识别message对应方
- agent必须是独立的操作系统
- 通道支持双向通讯,另外也能够支持中断、polling两种方式,让agent选择
从agent到platform的消息分为两种,同步和异步,为A2P通道:
- 同步(synchronous),agent返回的时候对应的platform操作就已经完成了。platform返回操作结果命令也是通过agent到platform的通道,同一个通道完成这些操作
- 异步(asynchronoous),当platform完成后,会发送 delayed response给到agent告知对方工作完成,这是P2A通道。agent发送完消息后,立马得到platform的返回,然后释放通道继续做下一次传输
SCMI协议的整体应用框图,从SCMI规范截图如下:
图片
scmi transport,channel,agent的对应关系:
1. 一个scp可以有多个agent,agent是运行在操作系统,安全固件的软件或者一个使用scmi协议的设备。例如juno有如下代理,0保留给平台。
enum juno_scmi_agent_idx {
JUNO_SCMI_AGENT_IDX_OSPM = 1,
JUNO_SCMI_AGENT_IDX_PSCI,
JUNO_SCMI_AGENT_IDX_COUNT,
};
2. transport定义了scmi协议如何传输。比如shared memory。一个agent可以有多个A2P或P2A channel,channel是双向的,但是协议发起者(主)-接收者(从)关系是固定的。故若要使能通知功能,除了一个A2P channel外,还需要一个P2A channel分配给这个agent.
SCMI协议的message header定义如下,对应代码module/scmi/include/mod_scmi_std.h中定义
图片
[protocol_id]:
图片
[message id]:
message id是二级功能区分id算cmd,例如设置状态、获取状态等具体操作。如果有新增的协议,那里面0/1/2这三个message都必须按照协议走。
图片
[message type]:
Commands 的message type都是0。对于不支持的协议和message类型,platform都要回复 NOT_SUPPORTED
Delayed responses 类型都是2
Notifications 为3
传输层:
传输层文档也就定义了一种方式,mailbox方式(核间通讯的一种ip)。这种通讯的前提是系统能够在agents和platform之间存在共享内存(ddr和片上flash都行,最好是片上flash)。mailebox能够完美支持前面提到的通道的需求,中断、内存和完成中断等都能够,而且是软件可操控。比如下面流程指出的中断和polling方式:
图片
mailbox通讯怎么定义在flash里面的layout:
图片
3. Agent scmi消息处理流程
这里我们以一个protocol_id为0x11的power domain控制消息为例子进行说明:
图片
scp中scmi消息处理时序图
1) mhu模块-中断产生:scmi底层硬件对应的模块是mhu模块,当硬件收到agent的消息时候会产生中断,中断处理函数为mhu_isr。在该函数中通过中断源查表获取对应的设备和smt channel。然后调用transport模块的api(调用transport_channel->api->signal_message(transport_channel->id);)发送消息。
2)transport模块-获取通道上下文:signal_message api中通过channel id获取channel上下文信息,检查通道是否ready和locked,调用scmi模块的api 处理(channel_ctx->scmi_api->signal_message(channel_ctx->scmi_service_id);)。
3) scmi模块-产生处理事件:
•scmi的api函数signal_message中将该消息封装成事件,通过fwk_put_event发送一个fwk_event_light。(事件中source_id为scmi模块,.target_id 为上一级smt 中channel_ctx->scmi_service_id,也是scmi。所以让该事件是自己发给自己的)。因为event有队列,中断调用的api是实时的。在scmi的.process_event回调函数中处理上面的事件。
•首先通过scmi维护的scmi_ctx.service_ctx_table获取transport信息找到transport_api(msg_smt模块提供),然后读出scmi消息的头部(scmi_protocol_id、scmi_message_id、scmi_message_type、scmi_token)。
•然后通过get_agent_id(event->target_id, &agent_id)获取该scmi 协议的agent_id(OSPM、PSCI等),根据agent_id获取到agent_type(psci、ospi等)。
•最后根据scmi_protocol_id找到protocol(例如0x11是power domain处理),调用protocol->message_handler(protocol->id, event->target_id,payload, payload_size, ctx->scmi_message_id)执行相对应的protocol的消息处理函数。message_handler函数执行到了scmi_power_domain模块。
4)scmi_power_domain模块-解析scmi消息:.message_handle函数对消息进行检验,将进行权限判断,然后查表调用具体的消息处理函数handler_table[message_id](service_id, payload)。例如scmi_protocol_id为scmi_power_domain,scmi_message_type为MOD_SCMI_PD_POWER_STATE_SET,则处理函数为scmi_pd_power_state_set_handler。该函数中将会进行策略判断(大多数模块为空),然后调用scmi_pd_ctx.pd_api->set_state(pd_id, pd_power_state)进行power domain的set,pd_api对应power_domain模块中对外api函数。
5)power_domain模块-调用driver处理:power_domain模块的api set_state函数先组装了一个event发给pd_id,也就是自己。pd_process_event()函数进行处理,process_set_state_request()按照pd的树形结构对状态进行设置,然后调用initiate_power_state_transition()执行status = pd->driver_api->set_state(pd->driver_id, state);更新pd的状态,并拿到执行结果status 。这里driver_api是在product/juno/scp_ramfw/config_power_domain.c的struct fwk_element element_table变量中定义,可以看到为FWK_MODULE_IDX_JUNO_PPU中提供
6) juno_ppu模块-寄存器设置:根据ppu_id拿到ppu的上下文ppu_ctx,按照传入的state值(on或者off)执行status = ppu_set_state_and_wait(ppu_ctx, mode);最后执行reg->POWER_POLICY = (uint32_t)mode;进行寄存器设置生效。
7) scmi_power_domain模块-返回结果:最后调用scmi_pd_ctx.scmi_api->respond(service_id, &return_values,....)到scmi 模块。
8) scmi模块:scmi中api的respond函数将会通过service_id查表service_ctx_table获取transport信息,然后调用ctx->respond(ctx->transport_id, payload, size),为msg_smt模块中respond api()(注transport_id在config_scmi.c 中配置。指定transport为smt模块+smt内的具体channel element元素))。
9)transport模块:msg_smt模块中的respond api为smt_respond()函数。通过上一级传入的transport_id/channel_id的element_idx部分,查表smt_ctx.channel_ctx_table获取channel消息。 然后填充Shared Memory,并调用channel_ctx->driver_api->raise_interrupt(channel_ctx->driver_id)产生中断,通知agent。
10.)mhu模块产生中断:raise_interrupt()函数中,根据slot_id找到设备上下文,然后对寄存器进行设置reg->SET |= (1U << slot);。
从上面可以看到,scmi的处理流程基本是通用的,涉及到不同平台的就是最后硬件的设置,需要新建一个juno_ppu模块-寄存器设置,及其配置文件。
SCP中scmi协议处理:
系统支持两种agent:PSCI和OSPM,发来的SCMI消息根据protocol_id进行分类,然后根据message_id子命令找到合适的处理函数,最后根据message_type决定是否进行回复。 关于SCMI协议的一些参数定义可以参考代码:
module/scmi/include/mod_scmi_std.h
例如上面我们介绍过0x11 power domain,其他的处理过程相似可以通过下面表速查到相关模块,从模块的static int (*handler_table中根据message_id下标迅速找到处理函数:
protocol_id | 描述 | 涉及模块及处理代码 |
0x10 | Base protocol | module/scmi/src/mod_scmi_base.c |
0x11 | Power domain management protocol | module/scmi_power_domain/src/mod_scmi_power_domain.c |
0x12 | System power management protocol | module/scmi_system_power/src/mod_scmi_system_power.c |
0x13 | Performance domain management protocol | module/scmi_perf/src/mod_scmi_perf.c |
0x14 | Clock management protocol | module/scmi_clock/src/mod_scmi_clock.c |
0x15 | Sensor management protocol | module/scmi_sensor/src/mod_scmi_sensor.c |
0x16 | Reset domain management protocol | module/scmi_reset_domain/src/mod_scmi_reset_domain.c |
0x17 | Voltage domain management protocol | module/scmi_voltage_domain/src/mod_scmi_voltage_domain.c |
0x18 | Power capping and monitoring protocol | 不支持 |
0x19 | Pin Control protocol | 不支持 |
4. PPU的电源控制
0x11 | Power domain management protocol | module/scmi_power_domain/src/mod_scmi_power_domain.c |
0x12 | System power management protocol | module/scmi_system_power/src/mod_scmi_system_power.c |
0x11 pd,0x12 system是通过power domain模块,然后到PPU模块进行电源控制的。关于PPU可以去PCSA规范中查看,PPU是一个硬件模块,SCP通过PPU去控制具体的时钟、电源等硬件。PPU类型如下所示:
enum mod_pd_type {
MOD_PD_TYPE_CORE,
MOD_PD_TYPE_CLUSTER,
MOD_PD_TYPE_DEVICE,
MOD_PD_TYPE_DEVICE_DEBUG,
MOD_PD_TYPE_SYSTEM,
MOD_PD_TYPE_COUNT
};
这里举例CPU COER的电源硬件控制,其他的自己看代码。
MOD_PD_TYPE_CORE的处理api为core_pd_driver_api,如下:
static const struct mod_pd_driver_api core_pd_driver_api = {
.set_state = core_set_state,
.get_state = pd_get_state,
.reset = core_reset,
.prepare_core_for_system_suspend = core_prepare_core_for_system_suspend,
};
core_set_state:
首先根据ppu_id拿到上下文参数(config_juno_ppu.c中定义),然后根据要设置的state进行分开处理:
static int core_set_state(fwk_id_t ppu_id, unsigned int state) {
get_ctx(ppu_id, &ppu_ctx);
dev_config = ppu_ctx->config;
mode = pd_state_to_ppu_mode[state];
switch ((enum mod_pd_state)state) {
case MOD_PD_STATE_OFF:
//设置PPU状态,并等待生效
status = ppu_set_state_and_wait(ppu_ctx, mode);
//清空这个PPU对应的中断消息
status = clear_pending_wakeup_irq(dev_config);
//关闭这个PPU对应的中断消息
status = disable_wakeup_irq(dev_config);
//关闭软重启中断消息
status = fwk_interrupt_disable(dev_config->warm_reset_irq);
break;
case MOD_PD_STATE_SLEEP:
status = ppu_set_state_and_wait(ppu_ctx, mode);
status = clear_pending_wakeup_irq(dev_config);
status = enable_wakeup_irq(dev_config);
status = fwk_interrupt_disable(dev_config->warm_reset_irq);
break;
case MOD_PD_STATE_ON:
status = fwk_interrupt_clear_pending(dev_config->warm_reset_irq);
status = fwk_interrupt_enable(dev_config->warm_reset_irq);
status = ppu_set_state_and_wait(ppu_ctx, mode);
break;
default:
fwk_unexpected();
status = FWK_E_PANIC;
break;
}
//power_domain模块中api调用,对这个pd进行订阅的模块会收到电源变化通知
status = ppu_ctx->pd_api->report_power_state_transition(ppu_ctx->bound_id,
state);
return FWK_SUCCESS;
}·
ppu_set_state_and_wait(ppu_ctx, mode);中设置PPU的mode,首先mode的转化如下:
static enum ppu_mode pd_state_to_ppu_mode[] = {
[MOD_PD_STATE_OFF] = PPU_MODE_OFF,
[MOD_PD_STATE_SLEEP] = PPU_MODE_OFF,
[MOD_PD_STATE_ON] = PPU_MODE_ON,
[MOD_SYSTEM_POWER_POWER_STATE_SLEEP0] = PPU_MODE_MEM_RET,
};
ppu_set_state_and_wait()函数中,对于mode的设置:
static int ppu_set_state_and_wait(struct ppu_ctx *ppu_ctx, enum ppu_mode mode)
{
//对寄存器进行设置
reg = ppu_ctx->reg;
reg->POWER_POLICY = (uint32_t)mode;
//根据配置信息等待PPU设置完成
dev_config = ppu_ctx->config;
params.mode = mode;
params.reg = reg;
if (fwk_id_is_equal(dev_config->timer_id, FWK_ID_NONE)) {
while (!set_power_status_check(¶ms)) {
continue;
}
}
对于中断的控制通过framework/src/fwk_interrupt.c中对外函数
int fwk_interrupt_disable(unsigned int interrupt)
{
if (!initialized) {
return FWK_E_INIT;
}
return fwk_interrupt_driver->disable(interrupt);
}
fwk_interrupt_driver在arch/arm/arm-m/src/arch_nvic.c中实现:
static int disable(unsigned int interrupt)
{
if (interrupt >= irq_count) {
return FWK_E_PARAM;
}
NVIC_DisableIRQ((enum IRQn)interrupt);
return FWK_SUCCESS;
}
__STATIC_INLINE void __NVIC_DisableIRQ(IRQn_Type IRQn)
{
if ((int32_t)(IRQn) >= 0)
{
NVIC->ICER[(((uint32_t)IRQn) >> 5UL)] = (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL));
__DSB();
__ISB();
}
}
对硬件寄存器进行了设置。
其他:
SCP入门系列就算讲完了,有规范有源码,有一点缺陷就是没用qmeu运行起来,官方也没给出,只说用ARM的Fixed Virtual Platform (FVP)能运行,不熟悉操作起来估计有点费劲对PC要求也高,这个SCP也比较小众在大规模的SoC上才有应用,提出的挺早但是应用的还是不多。其实找一个qemu支持的板子,把代码改一改应该也能运行起来,有兴趣的可以自己尝试下。
本文转载自微信公众号「OS与AUTOSAR研究」,可以通过以下二维码关注。转载本文请联系公众号。