ARM SCP入門-AP與SCP通信
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)通過感知到當前的負載及相應的功耗,經過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為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)) {
/* 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_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研究」,可以通過以下二維碼關注。轉載本文請聯系公眾號。