多核异构核间通信详解 — IPC

由于MP157 是一款多核异构的芯片,其中既包含的高性能的A7 核及实时性强的M4 内核,那么这两种处理器在工作时,怎么互相协调配合呢?这就涉及到了核间通信的概念了。

IPCC (inter-processor communication controller) 用于处理器间的数据交换的通知。它提供了一种非阻塞的信号机制,并提供原子的方式进行信号发布和信息检索。注意,核间通信的共享内存缓冲区是在MCU 的SRAM 中分配的,它不是IPCC 外设的一部分。

外设简述

IPCC 外设提供了硬件支持,来管理两个处理器实例之间的处理器间通信。每个处理器拥有特定的寄存器区域和中断。有点像硬件信号量的功能。

IPCC 提供了六个双向通道信号。每个通道分为两个子通道,每个子通道提供从“发送方”处理器到“接收方”处理器的单向信号:

  • P1_TO_P2 子通道(从P1 发到P2)
  • P2_TO_P1 子通道(从P2 发到P1)
  • 子通道中包括如下功能:

  • 一个标志位,用于标识通道正在被占用和空闲的两种状态,这个标志被“发送方”处理器设置为被占用,并被“接收方”处理器清除。
  • 两个相关的中断(所有通道都共享):
    – RXO: RX 通道被占用,连接到“接收器”处理器。
    – TXF: TX 通道空闲,连接到“发送”处理器。
  • 带多路复用的中断掩码功能。
  • IPCC 支持以下信道的操作模式:
    – 单工通信方式:
    – 仅使用一个子信道。
    – 单向消息:“发送者”处理器将通信数据发布到内存中后,它将通道状态标志设置为已占用。当消息被处理时,“接收者”处理器清除该标志。

  • 半双工通讯方式:
    – 仅使用一个子信道。
    – 双向消息:“发送者”处理器将通信数据发布到内存中后,它将通道状态标志设置为已占用。当消息被处理并且响应在共享内存中可用时,“接收器”处理器将清除该标志。
  • 全双工通讯方式:
    – 子通道用于异步模式。
    – 通过将子通道状态标志设置为占用,任何处理器都可以异步发布消息。当消息被处理时,“接收者”处理器清除该标志。可以将这种模式视为给定通道上两个单工模式的组合。
  • 核间通信的模型如下:

    框架简述

    IPCC 作为核间通信的桥梁,它仅承担着通知的角色,负责消息的分发、中断的处理等。

    实际上,IPCC 外设这个角色只是多核异构核间通信中的一块,在我们使用多核异构核间通信时,往往不仅希望使用到核间的消息通知,还希望能在不同的核心中进行数据的交互(比如M4 核进行实时的AD 数据采集处理,完成后,M4 核可通过异构的框架将数据呈递给A7 核,A7 核再进行更复杂的应用)。那么在这个需求的驱动下,就出现了一些框架相互配合使用的情况了,下面我们就给大家介绍这些内核框架。

    RemoteProc framework

    远程处理器框架(RPROC、RemoteProc)允许不同的平台/体系结构控制(打开电源,加载固件,关闭电源)远程处理器,同时抽象出硬件差异。此外,它还提供监视和调试远程协处理器的服务。

    以MP157 为例,其RemoteProc 可分为两块,分别是A7 核端、M4 核端:

    remoteproc:这是远程处理器框架的通用部分(在MP157 中为A7 核端)。

    它的作用是:

    - 将ELF 固件加载到远程处理器内存中。
    - 解析固件资源表以设置关联的资源(例如IPC,内存分割和跟踪)。
    - 控制远程处理器的执行(启动,停止⋯)。
    - 提供监视和调试远程固件的服务。

    stm32_rproc:这是远程处理器平台驱动程序(在MP157 中为M4 核端)。

    它的作用是:

    - 将stm32 特定的功能(回调)注册到RPROC 框架。
    - 处理与远程处理器关联的平台资源(例如寄存器,看门狗,复位,时钟和存储器)。
    - 通过邮箱框架将通知(通知)转发到远程处理器。

    ST 官方参考资料:
    https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Linux_RPMsg_framework_overview

    RPMsg framework

    此小节为大家简述有关Linux RPMsg 框架的内容。RPMsg 框架是一个基于virtio 的消息总线,它允许本地处理器与系统上可用的远程处理器通信。

    此框架在多核异构中承担的角色如下图:

    Linux RPMsg 框架是在virtio 框架顶层上实现的消息传送框架,其用于主机和远程处理器进行通信。它基于virtio vring,可通过共享内存向远程CPU 发送消息或从远程CPU 接收消息。

    这些vring 是单向的,一个vring 专用于发送到远程处理器的消息,另一个vring 用于从远程处理器接收的消息。此外,共享缓冲区需要在两个处理器都可见的内存空间中创建。

    当新消息在共享缓冲区中等待时,会使用到另一个框架Linux Mailbox framework ,该框架将用于通知对应的Core。

    依靠这些框架,RPMsg 框架实现了基于不同通道的通信。通道可被文本名称标识,并有一个本地(“源”) 的RPMsg 地址和一个远程(“目的”) 的RPMsg 地址。

    在远程处理器端(MP157 则为M4 核),也必须使用RPMSG 框架。RPMSG 框架的实现存在几种解决方案,ST 建议使用OpenAMP 方案,并在SDK 中给出了示例。

    Github OpenAMP 框架.

    简单来说,MP157 的A7 核与M4 核,通过一个标准的RPMsg 框架来建立起联系,完成数据传递。

    具体原理可以参考:

    RPMsg-Messaging-Protocol .
    RPMsg-Communication-Flow .

    Linux 内核源码目录给出的rpmsg client 的示例代码位置如下:

    samples/rpmsg/rpmsg_client_sample.c

    rpmsg 框架Linux 内核驱动源码位于:

    drivers/rpmsg

    ST 官方参考资料:

    https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Linux_remoteproc_framework_overview

    Mailbox framework

    此小节为大家简述有关Linux 邮箱框架的内容。邮箱框架涉及异构多核系统的处理器间通信。

    此框架的结构如下图:

    邮箱框架被用于内核间进行消息或信号的交换,常用于主机和协处理器间。邮箱由以下模块组成:

  • 一个邮箱控制器(mailbox controller),依赖于硬件平台实现,比如MP157 的IPCC 外设:
    – 它负责配置和处理来自IPCC 外围设备的IRQ。
    – 它为邮箱客户端提供了通用API。

  • 一个邮箱客户端(mailbox client),负责发送或接收消息。

  • 关于此框架的权威描述,在内核文档中的如下目录:

    Documentation/mailbox.txt

    一般而言mailbox controller 和client 都由芯片厂商来负责实现,因为这依赖于外设。我们更常关注的,则是mailbox client 的创建和使用。

    ST 实现的mailbox client 代码位置如下:

    drivers/remoteproc/stm32_rproc.c

    在内核中还给出了一份mailbox client 的示例驱动代码,代码通过debugfs 子系统,将mailbox 的操作暴露给了用户空间,用户可以直接通过debugfs 来使用mailbox,进行消息在不同内核中的传递。

    mailbox 框架的设备树描述可参考内核源码文档:

    Documentation/devicetree/bindings/mailbox/mailbox.txt

    一个简单的mailbox client 设备树节点,可以参考内核源码目录:

    Documentation/devicetree/bindings/mailbox/sti-mailbox.txt

    内核源码目录给出的mailbox client 的示例代码位置如下:

    drivers/mailbox/mailbox-test.c

    ST 官方参考资料:

    https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Linux_Mailbox_framework_overview

    框架小结

    前面介绍了三个框架,它们并不是独立工作的,而是相互协调的,彼此关联。我们可以通过两张图来查看它们之间的关系。

    以RemoteProc 框架为主视角出发:

    可以理清三个框架的关系,RemoteProc 可以说是骨架,关联到了RPMsg 框架、Mailbox 框架。

    设备树插件描述

    IPCC 设备树节点

    设备树节点位于arch/arm/boot/dts/stm32mp157c.dtsi

    列表1: IPCC 设备树节点

    ipcc: mailbox@4c001000 {
    	compatible = "st,stm32mp1-ipcc";
    	#mbox-cells = <1>;
    	reg = <0x4c001000 0x400>;
    	st,proc-id = <0>;
    	interrupts-extended =
    		<&intc GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
    		<&intc GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
    		<&exti 61 1>;
    	interrupt-names = "rx", "tx", "wakeup";
    	clocks = <&rcc IPCC>;
    	wakeup-source;
    	power-domains = <&pd_core>;
    	status = "disabled";
    };
    

    使用节点位于arch/arm/boot/dts/stm32mp157a-basic.dts

    列表2: 使用IPCC 设备树节点

    &ipcc {
    	status = "okay";
    };
    

    设备树中的compatible =“st,stm32mp1-ipcc”属性,会匹配到drivers/mailbox/stm32-ipcc.c 驱动程序,驱动程序中会创建一个mbox controller。

    A7<–>M4 rproc 设备树节点

    设备树节点位于arch/arm/boot/dts/stm32mp157c.dtsi

    列表3: rproc 设备树节点

    m4_rproc: m4@0 {
    	compatible = "st,stm32mp1-rproc";
    	#address-cells = <1>;
    	#size-cells = <1>;
    
    	ranges = <0x00000000 0x38000000 0x10000>,
    			<0x30000000 0x30000000 0x60000>,
    			<0x10000000 0x10000000 0x60000>;
    	resets = <&rcc MCU_R>;
    	reset-names = "mcu_rst";
    	st,syscfg-pdds = <&pwr 0x014 0x1>;
    	st,syscfg-holdboot = <&rcc 0x10C 0x1>;
    	st,syscfg-tz = <&rcc 0x000 0x1>;
    	st,syscfg-rsc-tbl = <&tamp 0x144 0xFFFFFFFF>;
    	status = "disabled";
    
    	m4_system_resources {
    		compatible = "rproc-srm-core";
    		status = "disabled";
    	};
    };
    

    使用节点位于arch/arm/boot/dts/stm32mp157a-basic.dts

    列表4: 使用rproc 设备树节点

    &m4_rproc {
    	memory-region = <&retram>, <&mcuram>, <&mcuram2>, <&vdev0vring0>,
    				<&vdev0vring1>, <&vdev0buffer>;
    	mboxes = <&ipcc 0>, <&ipcc 1>, <&ipcc 2>;
    	mbox-names = "vq0", "vq1", "shutdown";
    	interrupt-parent = <&exti>;
    	interrupts = <68 1>;
    	interrupt-names = "wdg";
    	wakeup-source;
    	recovery;
    	status = "okay";
    };
    

    设备树中的compatible = “st,stm32mp1-rproc” 属性, 会匹配到drivers/remoteproc/stm32_rproc.c 驱动程序,驱动程序中会创建一个mbox client,并基于RemoteProc、RPMsg框架与mbox client 进行关联。

    实验代码简述

    这里我们就简单讲解一下M4 核端的代码和一些概念,更详细的内容则需大家自己研究了。

    rpmsg 框架下有通信端点的概念,数据在两个端点间传输。端点间的数据传输是rpmsg 框架下数据传输最原始的形式,我们可以在原始的数据传输形式上再做一层封装,抽象出一些特定类型的设备。

    每个端点注册的底层实现,就是一个内核设备的注册(使用的是平台总线模型),故注册的端点设备,可以利用到驱动的Probe 功能(具体实现详见drivers/rpmsg/rpmsg_core.c 300 行后内容)。

    在M4 端,通过调用openamp 库中的OPENAMP_create_endpoint 函数,并在调用时指定参数name(即为设备名称),即可在内核中注册一个对应的rpmsg 框架平台设备,该设备最终可以通过name(设备名称)来匹配到相应的A7 端内核驱动:

    所以Linux rpmsg 框架下使用平台总线模型与端点通讯的方式结合,给一些需要有特殊操作的自定设备,提供了支持的可能。比如异构间的通讯,可以封装成串口通讯模型。

    在我们提供的M4 内核固件的代码中,注册了两种Linux 内核自带的rpmsg 框架下,原生支持的设备模型,这两种设备类型是rpmsg-tty-channel、rpmsg-client-sample:

  • rpmsg-tty-channel:tty 终端设备,对应内核驱动源码drivers/rpmsg/rpmsg_tty.c,此
    驱动模块默认被编译进内核。
  • rpmsg-client-sample:框架原生的通讯方式测试设备(放在内核里作为演示该框架的Demo提供的),对应内核驱动源码samples/rpmsg/rpmsg_client_sample.c,此驱动默认被编译成模块,并放置在文件系统/lib/modules/4.19.94-stm-r1/kernel/samples/rpmsg/rpmsg_client_sample.ko 中,当设备与驱动发生匹配时,系统会
    自动insmod 该驱动模块。
  • 还有一种字符设备模型,rpmsg_chrdev,源码位于drivers/rpmsg/rpmsg_char.c ,我
    们的代码中未实验,可自行研究。
  • 在M4 核的代码中,还初始化了usart3 作为M4 内核的Log 输出串口,我们可以通过串口模块接入开发板上的usart3,来查看M4 内核输出的Log。最终工程代码会被用于生成ELF 固件,ELF固件即为程序,会运行在MP157 的M4 内核上。

    综上,通过原生的rpmsg 框架设备、/dev/ttyRPMSGx 节点以及M4 内核使用的usart3 资源,我们就可以进行简单的实验了。本实验的代码也比较简单,这里就讲解这么多。

    实验准备

    由于多核异构的框架是与处理器的架构紧密联系在一起的,所以一般这些框架驱动会由芯片厂商为我们提供好。野火MP157 开发板默认开启了这些驱动支持,并且开启了对应的设备树,我们直接进行使用就可以了。

    在前面我们提到了,M4 内核要与A7 内核通讯需要共用一个框架,那么M4 内核的运行的程序里,就需要有对应的框架代码,这个为大家提供的工程中已经包含。最终我们将代码生成的ELF固件,通过A7 内核的remoteproc 子系统加载到M4 内核上,即可做好前期的准备工作。

    生成ELF 固件的工程代码位于\linux_driver\framework_ipcc\STM32Cube_FW_MP1_V1.2.0\Projects\STM32MP157C-EV1\Applications\OpenAMP\OpenAMP_raw 目录下,感兴趣可自行研究,工程可用MDK 或CubeIDE 打开(在工程目录中由对应文件夹)。


    重要: 在M4 核的代码中,还初始化了usart3,实验前请务必将usart3 的设备树插件关闭。


    实验操作

    M4 核的固件我们已经成功编译并放在了/linux_driver/framework_ipcc 目录下,我们将M4 核的固件OpenAMP_raw_CM4.elf 上传至Linux 文件系统的/lib/firmware/ 目录。此目录存放着Linux 系统中会使用到的各种固件。

    执行如下命令指定M4 内核加载的固件,默认在root 用户下操作:

    # 进入remoteproc 子系统目录
    cd /sys/class/remoteproc/remoteproc0
    # 导入M4 内核固件名称
    echo OpenAMP_raw_CM4.elf > firmware
    

    在同一目录下,执行如下命令可启动停止M4 内核:

    # 启动M4 内核
    echo start > state
    # 停止M4 内核
    echo stop > state
    

    启动M4 内核后信息如下:

    M4 内核加载固件并启动后,在串口终端中打印出了一些信息,我们通过串口模块接入usart3引脚,再打开串口调试助手设置波特率为115200,可以看到M4 固件初始化的usart3 作为串口printf 出来的信息,为[INFO ]M4 send to A7 : hello world! ,并且A7 端的驱动也打印出了rpmsg_client_sample virtio0.rpmsg-client-sample.-1.0: incomingmsg 1 (src: 0x0) 说明M4 核及A7 核驱动正常工作了。

    此外, 输入lsmod, 我们还可以看到演示设备创建后, 对应被动态加载的驱动模块,rpmsg_client_sample :

    下面,我们进行第二个设备测试,通过前面现象中的LOG,我们可以看到被枚举出的tty 设备节点/dev/ttyRPMSG0 节点,我们就通过该节点测试tty 设备的功能,输入如下命令:

    echo "hello M4 core , i'm A7!" > /dev/ttyRPMSG0
    

    实验现象如下所示:

    上图为A7 通过虚拟的tty 终端设备,转发到M4 内核的消息内容,最终通过M4 核固件的串口Log 功能打印出来对应信息。

    自此,所有实验结束。


    参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列

    物联沃分享整理
    物联沃-IOTWORD物联网 » 多核异构核间通信详解 — IPC

    发表评论