I.MX RT1170双核学习(1):双核通信之MU消息单元详解

2023-12-13 07:46:52

在I.MX RT1170中,它有CM7和CM4核,而消息单元(MU)模块使SoC内的两个处理器能够通过MU接口传递消息以进行通信和协调。

1 MU特性

MU包括以下特性:

  • 通过中断或轮询进行的消息控制
    • 中断也可用于从低功耗模式唤醒另一处理器
    • 允许一个处理器使用中断向另一个处理器发出信号
  • 对称的处理器接口,每一侧支持以下功能:
    • 四个通用中断请求,在另一核中反映
    • 三个通用标志,在另一核中反映
    • 四个带有可屏蔽中断的接收寄存器
    • 四个带有可屏蔽中断的发送寄存器
  • 由于CM7和CM4可能使用不同的时钟,MU需要确保在传递消息时两侧的访问是同步的,以避免数据传输或通信中的时序问题。
    • MU通过使用两组对应的寄存器来实现这种同步,以确保消息的正确传递和处理。
      在这里插入图片描述

2 功能描述

主要特性描述描述
处理器间中断CM7和CM4各有12个中断源,用于向另一处理器发出信号。这些中断可用于RX/TX事件的通知和处理器间的通用信号。
MU复位处理器A可通过其对应的控制寄存器(ACR)中的控制位(MUR)对整个MU进行复位。MUR位是自清零位。
核间状态和控制通信MU提供了一种方式,使两个核能够使用两个处理器的状态和控制寄存器进行通信。一个核的状态寄存器反映了另一个核的状态。控制寄存器用于控制操作,例如启用中断和向另一处理器发送中断。
核间同步消息传输通过同步机制更新两个核各自的传输和接收满标志实现。注意更新其中一个核的寄存器后,被另一个核接收到的过程存在延迟。
直接访问共享内存和避免冲突MU在两个核都提供了4个传输寄存器和4个接收寄存器。同时,两个核可以直接访问SoC的共享内存资源。为了访问共享内存的冲突(互斥),可以使用MU的中断和传输接收寄存器解决这个问题。
支持双核不同频率的时钟MU模块的核心是事件控制机制,MU制定了事件更新延迟,用于同步MU两侧的访问,因为两个核可以使用不同的时钟。
内存映射寄存器MU连接在双核各自的外设总线上

3 MU通信实例

MU向双核都提供了32位的状态和控制寄存器,用于控制操作(如中断、复位)以及检查另一侧的状态。对于消息传递,MU在两个处理器都提供了4个32位的只写传输寄存器和4个32位的只读接收寄存器。这些寄存器用于彼此发送消息,它们可以使用MU任一侧的控制和状态寄存器中提供的3个通用标志位进行控制。

通过MU,一个核可以传递一个32位的消息给另一个核,同时触发对方的中断。MU支持4个双向的通道:
在这里插入图片描述
下面通过SDK中的代码来看一下MU模块如何使用

3.1 轮训实现多核通信

这里以SDK中的evkmimxrt1170_mu_polling_core为例进行分析,它实现以下功能:

  1. core 0通过MU模块以轮询模式向core 1发送消息。
  2. core 1通过轮询模式将消息回发给core 0。
  3. core 0通过轮询模式接收来自core 1发送的消息。

这里core 0为CM7(主核),core 1为CM4(从核)。主核使用的MU模块成为MUA,从核使用的MU模块为MUB。

主从核实现:

下面一步步分析一下主从核代码的执行过程。

3.1.1 MU_SetFlags和MU_GetFlags

先来看一下初始化过程:

主核代码流程从核代码流程
初始化主核MU时钟:MU_Init(MUA)-
从CM7启动CM4核:设置向量表,复位等:APP_BootCore1()-
等待从核准备好:while (BOOT_FLAG != MU_GetFlags(MUA))初始化从核MU时钟:MU_Init(MUB)
-设置主核Flag指示从核已经运行:MU_SetFlags(MUB, BOOT_FLAG);

MU_SetFlags

这里主核在启动从核后等待从核置位,而从核启动后则调用MU_SetFlags置位。下面来看一下这个函数:

void MU_SetFlags(MU_Type *base, uint32_t flags)
{
    while (0U != (base->SR & ((uint32_t)MU_SR_FUP_MASK))){}
    MU_SetFlagsNonBlocking(base, flags);
}

static inline void MU_SetFlagsNonBlocking(MU_Type *base, uint32_t flags)
{
    uint32_t reg = base->CR;
    reg          = (reg & ~((MU_CR_GIRn_MASK | MU_CR_NMI_MASK) | MU_CR_Fn_MASK)) | MU_CR_Fn(flags);
    base->CR     = reg;
}

先来看一下最终调用的MU_SetFlagsNonBlocking函数:

#define MU_CR_GIRn_MASK                          (0xF0000U)
#define MU_CR_NMI_MASK                           0U
#define MU_CR_Fn_MASK                            (0x7U)
#define MU_CR_Fn(x)                              (((uint32_t)(((uint32_t)(x)) << 0)) & 7)
static inline void MU_SetFlagsNonBlocking(MU_Type *base, uint32_t flags)
{
    uint32_t reg = base->CR;
    reg          = (reg & ~((MU_CR_GIRn_MASK | MU_CR_NMI_MASK) | MU_CR_Fn_MASK)) | MU_CR_Fn(flags);
    base->CR     = reg;
}

这里将GIRnFn的位都清零了,然后根据flag的值再置Fn的位:

在这里插入图片描述

可以看到GIRn是用于中断通知MUA的,这里我们用的是轮询方式,所以清零。对于Fn位来说:

  • Fn的3位分别代表MUBMUA发送的不同标志
  • Fn位在MU重置(系统初始化或其它条件)的时候会清零,或者直接写000也能清零
  • MUA可以通过其SR寄存器的Fn位来获取MuB发送过来的标志

在这里插入图片描述

所以这里的MU_SetFlagsNonBlocking实际上就是置CR寄存器的Fn位。我们在程序中将其置为BOOT_FLAG,也就是三个标志位的最低位为1。

#define BOOT_FLAG 0x01U

另外在从核设置标志位之前,需要等待其MUB->SR寄存器的FUP标志位置0,来看一下这个位的定义:

在这里插入图片描述

也就是说如果之前MUB设置的标志位还没有update到MUA中,FUP为1,且此时修改CRFn位也是无效的,我们需要等待其自动清零后才能置标志位。

MU_GetFlags

#define MU_SR_Fn_MASK                            (0x7U)
#define MU_SR_Fn_SHIFT                           (0U)
static inline uint32_t MU_GetFlags(MU_Type *base)
{
    return (base->SR & MU_SR_Fn_MASK) >> MU_SR_Fn_SHIFT;
}

前面有提到MUA需要从SR寄存器的低三位获取MUB传来的标志位,这个函数就是获取SR的低三位。如果BOOT_FLAG相匹配,则程序继续往下执行。

3.1.2 MU_SendMsg和MU_ReceiveMsg

主核代码流程从核代码流程
发送消息给MUB:MU_SendMsg(MUA, kMU_MsgReg0, g_msgSend[i]);-
-接收MUA的消息:MU_ReceiveMsg(MUB, kMU_MsgReg0);
-回显收到的消息:MU_SendMsg(MUB, kMU_MsgReg0, g_msgRecv[i]);
MU_ReceiveMsg(MUA, kMU_MsgReg0);-

我们知道MU有4个双向的通信通道,这里就利用通道0进行主从核的通信:主核发从核收,然后从核回显信息给主核。

MU_SendMsg

void MU_SendMsg(MU_Type *base, uint32_t regIndex, uint32_t msg)
{
    while (0U == (base->SR & (((uint32_t)kMU_Tx0EmptyFlag) >> regIndex))){}
    base->TR[regIndex] = msg;
}

typedef enum _mu_msg_reg_index  //regIndex的取值,对应4个通道
{
    kMU_MsgReg0 = 0,
    kMU_MsgReg1,
    kMU_MsgReg2,
    kMU_MsgReg3,
} mu_msg_reg_index_t;

发送之前我们需要等待对应MUSR寄存器的[23:20]位的TEn(发送寄存器空)标志,四个位就对应四个通道。当消息发送到另一核后,该位会置0,当该位置1时,表示我们可以继续发送数据了。

在这里插入图片描述

  • 上图为MUA寄存器的说明,MUB类似

接着我们只要将数据写入TR寄存器即可,四个通道各有一个32位的TR寄存器:

在这里插入图片描述

来看一下MUA中的TR0寄存器的说明,TR1~TR3类似:

在这里插入图片描述

  • 写入MUATR0寄存器的数据会反映在MUBRR0中,这些寄存器都不是双缓冲的,所以数据会覆盖
  • TR0会清除MUASR中的TE0位,并置MUBSR中的RF0(接收满)位
  • TR0寄存器的任何写操作都将更新所有状态信息。

MU_ReceiveMsg

uint32_t MU_ReceiveMsg(MU_Type *base, uint32_t regIndex)
{
    while (0U == (base->SR & (((uint32_t)kMU_Rx0FullFlag) >> regIndex))){}
    return base->RR[regIndex];
}

前面有提到,MUA发来数据后,会置MUBSR中的RF0(接收满)位。

在这里插入图片描述

所以我们等待RF0位被置位,然后获取消息即可。消息从RR寄存器获取,同样地,四个寄存器对应四个通道:

在这里插入图片描述

其中RR0寄存器的描述如下:

在这里插入图片描述

3.1.3 调试从核注意事项

  • 这篇文章就不说明如何调试双核了,后面我会写一篇文章来讲解。

这里主要是双核调试有一个问题:我们通常首先启动主核,初始化系统,然后启动次核运行。在同时调试双核的情况下,调试器会启动次核。然后,在主核初始化尚未完成的情况下,次核可能会提前开始运行。

这里,我们使用RT1170的SRC(System Reset Controller)中的GPR(General Purpose Register)指示从核是否可以运行。如下图所示,这个寄存器对双核都可见,除了第0,1,2,3,4,9个GPR被ROM BootLoader使用外,其它的我们可以用来设置标志位,这里我们使用GPR20

在这里插入图片描述

次核在启动时应检查并等待SRC->GPR中的标志,主核在其初始化工作完成时在SRC->GPR中设置该标志。

主核在启动从核后执行以下代码

#define BOARD_SECONDARY_CORE_GO_FLAG 0xa5a5a5a5u
#define BOARD_SECONDARY_CORE_SRC_GPR kSRC_GeneralPurposeRegister20

SRC->GPR[BOARD_SECONDARY_CORE_SRC_GPR] = BOARD_SECONDARY_CORE_GO_FLAG;

从核在上电后执行以下代码

#define BOARD_SECONDARY_CORE_GO_FLAG 0xa5a5a5a5u
#define BOARD_SECONDARY_CORE_SRC_GPR kSRC_GeneralPurposeRegister20

while (BOARD_SECONDARY_CORE_GO_FLAG != SRC->GPR[BOARD_SECONDARY_CORE_SRC_GPR]){}  // 等待主核置位
SRC->GPR[BOARD_SECONDARY_CORE_SRC_GPR] = 0x0; // 用完后恢复GPR20的初始值0,防止主从核软件复位后,从核又提前运行

3.2 中断实现多核通信

和刚刚轮询实现的功能一样,我们来学习一下如何使用中断来收发数据。
这里使用中断的方式实现与刚刚轮询代码一样的功能,整体代码类似,下面来梳理一下中断需要做的操作:

主核

1、使能中断

(1)NVIC使能

NVIC_EnableIRQ(MUA_IRQn);

(2)使能中断标志位:发送和接收中断

MU_EnableInterrupts(MUA, (kMU_Tx0EmptyInterruptEnable | kMU_Rx0FullInterruptEnable));

(3)发送和接收数据

我们打开发送空中断后,就调用MU_SendMsgNonBlockingMUB发送消息,等这次发送完毕后,再次进入发送空中断则调用MU_DisableInterrupts禁用发送空中断。

同样地,等从核MUB发来消息后,进入接收满中断,然后调用MU_ReceiveMsgNonBlocking接收数据,等下次接收满时调用MU_DisableInterrupts关闭接收满中断。

#define MSG_LENGTH 32U
void APP_MU_IRQHandler(void)
{
    uint32_t flag = 0;

    flag = MU_GetStatusFlags(MUA);
    if ((flag & kMU_Tx0EmptyFlag) == kMU_Tx0EmptyFlag)
    {
        if (g_curSend < MSG_LENGTH)
        {
            MU_SendMsgNonBlocking(MUA, kMU_MsgReg0, g_msgSend[g_curSend++]);
        }
        else
        {
            MU_DisableInterrupts(MUA, kMU_Tx0EmptyInterruptEnable);
        }
    }
    if ((flag & kMU_Rx0FullFlag) == kMU_Rx0FullFlag)
    {
        if (g_curRecv < MSG_LENGTH)
        {
            g_msgRecv[g_curRecv++] = MU_ReceiveMsgNonBlocking(MUA, kMU_MsgReg0);
        }
        else
        {
            MU_DisableInterrupts(MUA, kMU_Rx0FullInterruptEnable);
        }
    }
    SDK_ISR_EXIT_BARRIER;
}

从核

整体流程和轮询代码一致,另外和主核一样要打开对应的中断,现在来看看MUB的中断回调函数:

void APP_MU_IRQHandler(void)
{
    uint32_t flag = 0;

    flag = MU_GetStatusFlags(APP_MU);
    if ((flag & kMU_Rx0FullFlag) == kMU_Rx0FullFlag)
    {
        if (g_curRecv < MSG_LENGTH)
        {
            g_msgRecv[g_curRecv++] = MU_ReceiveMsgNonBlocking(MUB, kMU_MsgReg0);
        }
        else
        {
            MU_DisableInterrupts(MUB, kMU_Rx0FullInterruptEnable);
        }
    }
    if (((flag & kMU_Tx0EmptyFlag) == kMU_Tx0EmptyFlag) && (g_curRecv == MSG_LENGTH))
    {
        if (g_curSend < MSG_LENGTH)
        {
            MU_SendMsgNonBlocking(MUB, kMU_MsgReg0, g_msgRecv[g_curSend++]);
        }
        else
        {
            MU_DisableInterrupts(MUB, kMU_Tx0EmptyInterruptEnable);
        }
    }
    SDK_ISR_EXIT_BARRIER;
}

同样地在使能发送空中断后,这里的中断就一直会被调用,但是这里的发送空分支中还判断了(g_curRecv == MSG_LENGTH),也就是MUB接收了MUA发送的完整的MSG_LENGTH(32)字节才允许进入这个分支,进入后将收到的数据回显给MUA,然后在下一次进入发送空中断时关闭中断。

对于接收满中断来说一样,收到MSG_LENGTH字节后,在下一次进入中断时关闭接收满中断。

文章来源:https://blog.csdn.net/tilblackout/article/details/134672293
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。