前言
本文重点关注 arm-scmi 作为许多子系统的 provide 与上层(consumer core)如何绑定,作为 SCMI 的发起者与 SCP 如何交流,以及自身和设备树如何配合进行初始化。
前置知识:ARM 电源管理框架 PCSA
公众号老秦谈芯: mp.weixin.qq.com/s/2NfYdpyC9...
SCMI 是电源管理核和 AP(linux)核的通信协议,使用 PCSA 进行调用的一次流程如下
本篇重点关注 Linux 的 arm-scmi 框架,实际应用中,consumer 的代码不用太管,arm-scmi 框架完全不用改代码,mailbox 通信机制和 SCP 固件中的 platform 相关代码是核心。SCP 中的代码会在之后整理。
正文
scmi 作用范围
在具体看设备树前先看下 scmi 的适用范围,scmi 框架是 Linux 中子系统的 provider
将场馆的内核中 provider 具体实现,放在了 SCP firmware 中
ini
enum scmi_std_protocol {
SCMI_PROTOCOL_BASE = 0x10,
SCMI_PROTOCOL_POWER = 0x11,
SCMI_PROTOCOL_SYSTEM = 0x12,
SCMI_PROTOCOL_PERF = 0x13,
SCMI_PROTOCOL_CLOCK = 0x14,
SCMI_PROTOCOL_SENSOR = 0x15,
SCMI_PROTOCOL_RESET = 0x16,
SCMI_PROTOCOL_VOLTAGE = 0x17,
SCMI_PROTOCOL_POWERCAP = 0x18,
};
除了 BASE 承担 arm-scmi 框架自身的功能,0X12 SYSTEM 在官方的设备树绑定里都没写咋用,代码里更是没有,Power 是 SCMI 自身对 Power Domain 的实现。其他的每一项都会对应于 Linux 中的一个子系统,并成为这个子系统的 Provider。Voltage 对应 Regulator,Perf 对应 Cpufreq,Sensor 对应 HWMON,CLOCK RESET POWERCAP 都对应 Linux 下的同名子系统。
scmi 作为 platform device
scmi platform 的身份完成了
- 自己的注册,通过 compatible 确定了 mailbox 的使用方式
- Bus 的注册,方便后续子节点 device 和 driver 绑定
scmi 为了方便自己的 driver device 和设备树配合绑定,自己弄了个 bus,match 的条件也是对应上 Protocol ID 就 probe(scmi_dev_match_id()函数中)。先看一下在设备树中的 scmi 节点。
ini
firmware {
optee: optee {
compatible = "linaro,optee-tz";
method = "smc";
};
scmi: scmi {
compatible = "linaro,scmi-optee";
#address-cells = <1>;
#size-cells = <0>;
linaro,optee-channel-id = <0>;
scmi_clk: protocol@14 {
reg = <0x14>;
#clock-cells = = <1>;
};
scmi_reset: protocol@16 {
reg = <0x16>;
#reset-cells = <1>;
};
...
}
}
可以看到 scmi_clk scmi_reset 是 scmi 的子节点,scmi 是 platform_device,注册和 driver 的绑定按 platform 总线规则, 具有 compatible 属性。
和 SPI I2C 类似,scmi 自己作为 platform_device 和 driver probe 时会建立 scmi_bus,方便子节点设备和驱动绑定。
Compatible
Compatible 在这里决定了和 mailbox 侧对接的格式
arduino
_/* Each compatible listed below must have descriptor associated with it */_
static const struct **of_device_id** **scmi_of_match**[] = {
#ifdef **CONFIG_ARM_SCMI_TRANSPORT_MAILBOX**
{ .**compatible** = "**arm,scmi**", .data = &**scmi_mailbox_desc** },
#endif
#ifdef **CONFIG_ARM_SCMI_TRANSPORT_OPTEE**
{ .**compatible** = "**linaro,scmi-optee**", .data = &**scmi_optee_desc** },
#endif
#ifdef **CONFIG_ARM_SCMI_TRANSPORT_SMC**
{ .**compatible** = "**arm,scmi-smc**", .data = &**scmi_smc_desc**},
{ .**compatible** = "**arm,scmi-smc-param**", .data = &**scmi_smc_desc**},
#endif
#ifdef **CONFIG_ARM_SCMI_TRANSPORT_VIRTIO**
{ .**compatible** = "**arm,scmi-virtio**", .data = &**scmi_virtio_desc**},
#endif
{ _/* Sentinel */_ },
};
如果是 arm,scmi 则用 Linux 下的 mailbox 框架,如果是 linaro,scmi-optee 则要依赖安全 firmware 中的安全通道进行数据传输,.data 的内容如下
arduino
static const struct **scmi_transport_ops** **scmi_mailbox_ops** = {
.**chan_available** = **mailbox_chan_available**,
.**chan_setup** = **mailbox_chan_setup**,
.**chan_free** = **mailbox_chan_free**,
.**send_message** = **mailbox_send_message**,
.**mark_txdone** = **mailbox_mark_txdone**,
.**fetch_response** = **mailbox_fetch_response**,
.**fetch_notification** = **mailbox_fetch_notification**,
.**clear_channel** = **mailbox_clear_channel**,
.**poll_done** = **mailbox_poll_done**,
};
const struct **scmi_desc** **scmi_mailbox_desc** = {
.ops = &**scmi_mailbox_ops**,
.**max_rx_timeout_ms** = 30, _/* We may increase this if required */_
.**max_msg** = 20, _/* Limited by MBOX_TX_QUEUE_LEN */_
.**max_msg_size** = 128,
};
ops 定义了 scmi 数据传输的方式,下面代码为 scmi_mailbox 方式,mbox_send_message 为 Linux include 下的声明,没有继续深入了。
ini
static int mailbox_send_message(struct scmi_chan_info *cinfo,
struct scmi_xfer *xfer)
{
struct scmi_mailbox *smbox = cinfo->transport_info;
int ret;
ret = mbox_send_message(smbox->chan, xfer);
/* mbox_send_message returns non-negative value on success, so reset */
if (ret > 0)
ret = 0;
return ret;
}
Bus
- init 阶段
bus_register 创造 bus 的标志,在 init 阶段即完成 bus 的创建
- probe 阶段
scmi 在作为 platform_device probe 的过程中将解析子节点,并将子节点的 device 创建出来。
scss
static int __init scmi_bus_init(void)
{
int retval;
retval = bus_register(&scmi_bus_type);
if (retval)
pr_err("SCMI protocol bus register failed (%d)\n", retval);
pr_info("SCMI protocol bus registered\n");
return retval;
}
protocol
init 阶段还做了一件事情,需要把 protocol 创建出来,protocol 是 scmi_bus 和 scmi_device/driver 之间的桥梁。因为框架原因,本来所有的 clock reset 信息都要在 provider 的 c 文件中存一份,编号在设备树的 .h 中存一份。现在这两份都不存在了,设备树绑定的时候需要的信息需要从 SCP 侧拿。之前说过 scmi 框架是 内核子系统的 provider ,故在 scmi_device 和 driver 绑定的过程中,我们需要把 provider 必备的信息都获取到,最基础的比如,总共有几个 clock reset ?
Protocol 的核心有两个数据结构,一个描述自身(scmi_protocol ),一个描述自身提供的功能(scmi_reset_proto_ops )
instance_init 是初始化时调用的,会从 SCP 拿点信息回来,reset assert deassert 和内核中 reset 子系统的 api 对应,就是 consumer 调用过来的处理函数,最后都会调用到 SCP,其他的几个是从 SCP 侧拿数据回来,之前 instance_init 时拿到的信息这时候用就行了,不用再去拿了。
ini
// linux-6.6.1/drivers/firmware/arm_scmi/reset.c
static const struct scmi_protocol scmi_reset = {
.id = SCMI_PROTOCOL_RESET,
.owner = THIS_MODULE,
.instance_init = &scmi_reset_protocol_init,
.ops= &reset_proto_ops,
.events = &reset_protocol_events,
};
static const struct scmi_reset_proto_ops reset_proto_ops = {
.num_domains_get = scmi_reset_num_domains_get,
.name_get = scmi_reset_name_get,
.latency_get = scmi_reset_latency_get,
.reset = scmi_reset_domain_reset,
.assert = scmi_reset_domain_assert,
.deassert = scmi_reset_domain_deassert,
};
driver
有了 protocol 就已经有了作为 provider 的功能实现,现在还差怎么把自己 register 到 core 中。Driver 实现这个功能,重要的数据结构也是描述自己的和描述功能的,这里的功能完全对应 protocol 中的功能,作为 ops 提供给 core,完成 protocol 到 core 的连接。
ini
// linux-6.6.1/drivers/reset/reset-scmi.c
static struct scmi_driver scmi_reset_driver = {
.name = "scmi-reset",
.probe = scmi_reset_probe,
.id_table = scmi_id_table,
};
static const struct reset_control_ops scmi_reset_ops = {
.assert = scmi_reset_assert,
.deassert= scmi_reset_deassert,
.reset = scmi_reset_reset,
};
这个 driver 的 probe 对应设备树里的 scmi 子节点,我们现在有了完成任务的信息和功能,只差组装一个 reset 的 device 被 register 到 core 里。probe 就做了这些事情
ini
static int scmi_reset_probe(struct scmi_device *sdev)
{
struct scmi_reset_data *data;
struct device *dev = &sdev->dev;
struct device_node *np = dev->of_node;
const struct scmi_handle *handle = sdev->handle;
struct scmi_protocol_handle *ph;
if (!handle)
return -ENODEV;
reset_ops = handle->devm_protocol_get(sdev, SCMI_PROTOCOL_RESET, &ph);
if (IS_ERR(reset_ops))
return PTR_ERR(reset_ops);
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->rcdev.ops = &scmi_reset_ops;
data->rcdev.owner = THIS_MODULE;
data->rcdev.of_node = np;
data->rcdev.nr_resets = reset_ops->num_domains_get(ph);
data->ph = ph;
return devm_reset_controller_register(dev, &data->rcdev);
}
16 到 26 行是所有 reset provider 的经典写法,唯一不同的是,其他的 provider 信息要从设备树和 .c 里面拿,scmi 框架直接从 protocol 拿就可以。值得注意的是在 devm_protocol_get 调用过程中,才会调用 protocol 的 init,同时可以看到最关键的,对上层的数据结构 ph ,scmi_protocol_handle ph 是每个 protocol 对应一个,通过它可以找到该 protocol 被正常调用的所需要的信息。类似 dma_async_tx_descriptor 在 DMA 框架中的作用。
在 Linux 下一次调用过程
最后以一次从 consumer 到 arm-scmi 的过程收尾。
Consumer 设备树,表明 clock index
ini
rtc: rtc@5c004000 {
compatible = "st,stm32mp1-rtc";
reg = <0x5c004000 0x400>;
clocks = <&scmi_clk CK_SCMI_RTCAPB>,
<&scmi_clk CK_SCMI_RTC>;
clock-names = "pclk", "rtc_ck";
status = "disabled";
};
Consumer 驱动,请求 clock
ini
// linux-6.6.1/drivers/rtc/rtc-stm32.c stm32_rtc_probe()
rtc->pclk = devm_clk_get(&pdev->dev, "pclk");
Consumer 驱动,使用 clock
ini
// /home/mindie/linux-6.6.1/drivers/rtc/rtc-stm32.c stm32_rtc_init()
rate = clk_get_rate(rtc->rtc_ck);
Clk core 分发 api 从 clk_get_rate 到 clk_recalc 中间都是 clk core 的 API 中转
arduino
// linux-6.6.1/drivers/clk/clk.c
static unsigned long clk_recalc(struct clk_core *core,
unsigned long parent_rate)
{
unsigned long rate = parent_rate;
if (core->ops->recalc_rate && !clk_pm_runtime_get(core)) {
rate = core->ops->recalc_rate(core->hw, parent_rate);
clk_pm_runtime_put(core);
}
return rate;
}
Provider (scmi driver) 驱动 ops
arduino
// linux-6.6.1/drivers/clk/clk-scmi.c
static const struct clk_ops scmi_clk_ops = {
.recalc_rate = scmi_clk_recalc_rate,
}
Scmi driver ops 调用 proto ops
arduino
// linux-6.6.1/drivers/clk/clk-scmi.c
static unsigned long scmi_clk_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
int ret;
u64 rate;
struct scmi_clk *clk = to_scmi_clk(hw);
ret = scmi_proto_clk_ops->rate_get(clk->ph, clk->id, &rate);
if (ret)
return 0;
return rate;
}
Protocol ops
ini
// linux-6.6.1/drivers/firmware/arm_scmi/clock.c
static const struct scmi_clk_proto_ops clk_proto_ops = {
.count_get = scmi_clock_count_get,
.rate_get = scmi_clock_rate_get,
}
Protocol 调用 mailbox
scss
// linux-6.6.1/drivers/firmware/arm_scmi/clock.c
static int
scmi_clock_rate_get(const struct scmi_protocol_handle *ph,
u32 clk_id, u64 *value)
{
int ret;
struct scmi_xfer *t;
ret = ph->xops->xfer_get_init(ph, CLOCK_RATE_GET,
sizeof(__le32), sizeof(u64), &t);
if (ret)
ret;
put_unaligned_le32(clk_id, t->tx.buf);
ret = ph->xops->do_xfer(ph, t);
if (!ret)
*value = get_unaligned_le64(t->rx.buf);
ph->xops->xfer_put(ph, t);
return ret;
}
看到 ph 可以知道已经进入 scmi 框架,ph->xops->do_xfer 后调用到 compatible 那里规定好的传输方式中,即 scmi_desc,后调用 mailbox 相关函数,至此已不在 arm-scmi 框架中。
以 clock 举例,其他适用 Linux 子系统的都类似以上步骤