ARM SCP入门-AP与SCP通信

电子说

1.3w人已加入

描述

 

    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入门-安全固件软件介绍和代码运行,在EL3中处理的程序是BL31,把SMC系统调用的参数转化为PSCI协议去执行,这时如果有SCP那A核就憋屈了,自己没权利执行需要通过SCMI协议上报给SCP了。这就是整个过程的软件协议栈如上图中:
  • 用户层:首先用户发起的一些操作,通过用户空间的各service处理,会经过内核提供的sysfs,操作cpu hotplug、device pm、EAS、IPA等。
  • 内核层:在linux内核中,EAS(energy aware scheduling)通过感知到当前的负载及相应的功耗,经过cpuidle、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 controlprocessor,简称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参与运行管理,这也就需要SCPAP之间有一个通信接口。这个通信接口在硬件上可以通过共享存储和MHUMessage HandlingUnit)实现;在软件上,通过定义一组通信协议来实现。

 

主要涉及的模块如下:

 

  • mhu模块Message HandlingUnit (MHU)module/mhu/src/mod_mhu.c中实现

     

  • msg_smt模块Shared MemoryTransport 是一种用于描述系统内存拓扑的数据结构。在ARM 架构中,SCP 固件使用 Shared MemoryTransport来提供有关系统内存的信息,如地址范围、类型、属性等。SystemMemory Tables 通常由系统固件在启动过程中生成,并由SCP 固件和其他系统组件使用。它们允许系统软件了解和管理系统中可用的内存资源。

     

  • SCMI模块System Control &Management Interface (SCMI)

     

  • 业务处理模块,为scmi protocol模块例如scmi_power_domain

     

    SCMI抽象出协议传输两层,协议层描述能够支持的命令,传输层定义了命令通过什么方式传输,发送命令方称为agent。有个限制,每个agent的传输通道必须一个或者多个,然后如果有安全需求,那安全AP必须使用安全的通道进行传输数据。寄存器协议层:

 

  • 通道(channel)必须是分开独立的,各个agent不能使用同一个。避免platform无法识别message对应方

     

  • agent必须是独立的操作系统

     

  • 通道支持双向通讯,另外也能够支持中断、polling两种方式,让agent选择

     

agentplatform的消息分为两种,同步和异步,为A2P通道:

 

  • 同步(synchronous),agent返回的时候对应的platform操作就已经完成了。platform返回操作结果命令也是通过agentplatform的通道,同一个通道完成这些操作

     

  • 异步(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可以有多个A2PP2A channelchannel是双向的,但是协议发起者(主)-接收者(从)关系是固定的。故若要使能通知功能,除了一个A2P channel外,还需要一个P2A channel分配给这个agent.

 

SCMI协议的message header定义如下,对应代码module/scmi/include/mod_scmi_std.h中定义

 

寄存器[protocol_id]

 

寄存器[message id]:

 

message id是二级功能区分idcmd,例如设置状态、获取状态等具体操作。如果有新增的协议,那里面0/1/2这三个message都必须按照协议走。

 

寄存器  [message type]:

 

Commands message type都是0。对于不支持的协议和message类型,platform都要回复 NOT_SUPPORTED

 

Delayed responses 类型都是2

 

Notifications 3

 

传输层:

 

    传输层文档也就定义了一种方式,mailbox方式(核间通讯的一种ip)。这种通讯的前提是系统能够在agentsplatform之间存在共享内存ddr和片上flash都行,最好是片上flash)。mailebox能够完美支持前面提到的通道的需求,中断、内存和完成中断等都能够,而且是软件可操控。比如下面流程指出的中断和polling方式:

 

寄存器  mailbox通讯怎么定义在flash里面的layout:

 

寄存器   3. Agent scmi消息处理流程    这里我们以一个protocol_id0x11power domain控制消息为例子进行说明:

 

寄存器scpscmi消息处理时序图

 

1. mhu模块-中断产生scmi底层硬件对应的模块是mhu模块,当硬件收到agent的消息时候会产生中断,中断处理函数为mhu_isr。在该函数中通过中断源查表获取对应的设备和smtchannel。然后调用transport模块的api(调用transport_channel->api->signal_message(transport_channel->id);)发送消息。

 

2. transport模块-获取通道上下文:signal_messageapi中通过channel id获取channel上下文信息,检查通道是否readylocked,调用scmi模块的api 处理(channel_ctx->scmi_api->signal_message(channel_ctx->scmi_service_id);)。

 

3. scmi模块-产生处理事件

 

scmiapi函数signal_message中将该消息封装成事件,通过fwk_put_event发送一个fwk_event_light。(事件中source_idscmi模块,.target_id 为上一级smt channel_ctx->scmi_service_id,也是scmi。所以让该事件是自己发给自己的)。因为event有队列,中断调用的api是实时的。在scmi.process_event回调函数中处理上面的事件。

 

首先通过scmi维护的scmi_ctx.service_ctx_table获取transport信息找到transport_apimsg_smt模块提供),然后读出scmi消息的头部(scmi_protocol_idscmi_message_idscmi_message_typescmi_token)。

 

然后通过get_agent_id(event->target_id, &agent_id)获取该scmi 协议的agent_idOSPMPSCI等),根据agent_id获取到agent_typepsciospi等)。

 

最后根据scmi_protocol_id找到protocol(例如0x11power 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_idscmi_power_domainscmi_message_typeMOD_SCMI_PD_POWER_STATE_SET,则处理函数为scmi_pd_power_state_set_handler。该函数中将会进行策略判断(大多数模块为空),然后调用scmi_pd_ctx.pd_api->set_state(pd_id,pd_power_state)进行power domainset,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.cstruct fwk_elementelement_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模块:scmiapirespond函数将会通过service_id查表service_ctx_table获取transport信息,然后调用ctx->respond(ctx->transport_id,payload, size),为msg_smt模块中respond api()(注transport_idconfig_scmi.c 中配置。指定transportsmt模块+smt内的具体channel element元素))。

 

9.transport模块:msg_smt模块中的respond apismt_respond()函数。通过上一级传入的transport_id/channel_idelement_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模块-寄存器设置,及其配置文件。

 

SCPscmi协议处理:

 

    系统支持两种agentPSCIOSPM,发来的SCMI消息根据protocol_id进行分类,然后根据message_id子命令找到合适的处理函数,最后根据message_type决定是否进行回复。
    关于SCMI协议的一些参数定义可以参考代码:module/scmi/include/mod_scmi_std.h    例如上面我们介绍过0x11 powerdomain,其他的处理过程相似可以通过下面表速查到相关模块,从模块的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 pd0x12 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的处理apicore_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);中设置PPUmode,首先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(¶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_driverarch/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的了,现在都是把电脑系统和常用软件都换英文显示的了,努力看英文无障碍。  

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分