电子说
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了。这就是整个过程的软件协议栈如上图中:
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设备上使用不同特权级别的监控软件。该接口旨在在以下电源管理场景中代码通用化:主要涉及的模块如下:
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, };
|
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。在该函数中通过中断源查表获取对应的设备和smtchannel。然后调用transport模块的api(调用transport_channel->api->signal_message(transport_channel->id);)发送消息。
2. transport模块-获取通道上下文:signal_messageapi中通过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_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模块: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决定是否进行回复。
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
|
不支持
|
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
|
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 };
|
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, };
|
首先根据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; }·
|
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, };
|
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; } }
|
int fwk_interrupt_disable(unsigned int interrupt) { if (!initialized) { return FWK_E_INIT; } return fwk_interrupt_driver->disable(interrupt); }
|
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的了,现在都是把电脑系统和常用软件都换英文显示的了,努力看英文无障碍。
全部0条评论
快来发表一下你的评论吧 !