电源管理 ——Linux SCMI 框架

前言

本文重点关注 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 的身份完成了

  1. 自己的注册,通过 compatible 确定了 mailbox 的使用方式
  2. 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 子系统的都类似以上步骤

相关推荐
梅见十柒5 分钟前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热8 分钟前
路由基础(全)
linux·网络·网络协议·安全
传而习乎18 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
我们的五年28 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
IT果果日记1 小时前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教1 小时前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux
小陈phd2 小时前
Vscode LinuxC++环境配置
linux·c++·vscode
是阿建吖!2 小时前
【Linux】进程状态
linux·运维
明明跟你说过2 小时前
Linux中的【tcpdump】:深入介绍与实战使用
linux·运维·测试工具·tcpdump