ARM SCP入门-AP与SCP通信

原创 thatway 那路谈OS与SoC嵌入式软件 2023-07-16 08:30

**SoC**上有很多核,ATF和Linux占据了**A核**,SCP占据了一个**M核**,当遇到Linux没有权限的事情的时候(**SMC**进入EL3转**PSCI**协议,例如电源管理),就需要给SCP**打报告**,SCP审批完**批条子**后去执行。这其中涉及到了**异构核间通信**,估计第一时间会想到**mailbox**,不过mailbox算是一个**传输层**,面向的是bit位数据的传输,可以把这些传输数据组织成一个**协议层**,在AP与SCP的核间通信中那就是**SCMI**。  

1. SMC系统调用与PSCI协议

当Linux想要关机或者休眠的时候,这涉及到整个系统电源状态的变化,为了**安全性**Linux内核没有权利去直接执行了,需要陷入到**EL3等级**去执行,可以参考之前文章[ARM ATF入门-安全固件软件介绍和代码运行](http://mp.weixin.qq.com/s?__biz=MzUzMDMwNTg2Nw==&mid=2247484384&idx=1&sn=c6a2c66b967a28f8f46430263bad7df6&chksm=fa5285c4cd250cd27a333f15bfcef80e8a8f92ac9afe8ac766f93e75a0dbc7500de2d4df0eff&scene=21#wechat_redirect)  

,在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 {
    /* 0 is reserved for the platform */
    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为0x11power 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模块。

  1. scmi_power_domain**模块-解析scmi消息**:
    .message_handle函数对消息进行检验,将进行权限判断,然后查表调用具体的消息处理函数
    handler_tablemessage_id。例如
    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函数。

  2. 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中提供

  3. 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;进行寄存器设置生效。

  4. scmi_power_domain**模块-返回结果:**最后调用
    scmi_pd_ctx.scmi_api->respond(service_id, &return_values,....)到
    scmi 模块。

  5. 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。

  1. mhu**模块产生中断**:
    raise_interrupt()函数中,根据
    slot_id找到设备上下文,然后对寄存器进行设置
    reg->SET |= (1U << slot);。

从上面可以看到,
scmi的处理流程基本是通用的,涉及到不同平台的就是最后硬件的设置,需要新建一个juno_ppu**模块-寄存器设置,及其配置文件。**

SCP**scmi协议处理:**

系统支持两种  

agent:PSCIOSPM,发来的
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_pp**u.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)) {
/* Wait for the PPU to set */
while (!set_power_status_check(&params)) {
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支持的板子,把代码改一改应该也能运行起来,有兴趣的可以自己尝试下。  

后记:

**英文规范**+**源码**才是**一手资料**,**看二手资料永远都跟不上别人**,比如知乎、CSDN、公众号、bilibili等中文的总结文档,甚至我这篇博客。为什么会这样?因为英文规范很全面,总结出来的二手中文文档只是翻译了其中一部分,但是那个**写二手文档的人肯定把一手的都看了**,所以你看二手的因为**不全**而永远落后别人,**二手****好处就是入门快**,要精通还是看一手的吧。  

不过我这里尽量是简介和汇总文档,而不是大篇幅的摘抄翻译,让大家好找到出处,**知道去看什么英文文档,去哪里找**,一般就是**ARM官网**(本文的SMC、PSCI、SCMI)或者**github**。搞一些有点技术含量的研发特别是靠近底层软件和芯片技术的,**英文**是一道坎,中国没有只能**学学老外5-10前**的技术已经算先进的了,这些领域国内基本还是  

海归
或者外企待过的人把持,说话都夹杂着满嘴的英文单词行业术语缩写不装逼还真不是一个level**的了**,现在都是把电脑系统和常用软件都换英文显示的了,努力看英文无障碍。

啥都懂一点
啥都不精通

干啥都能干
干啥啥不是

专业入门劝退
堪称程序员杂家
”。

后续会继续更新,  

纯干货
分析,无广告,不打赏,欢迎
转载
,欢迎
评论交流

results matching ""

    No results matching ""