在I.MX RT1170中,它有CM7和CM4核,而消息单元(MU
)模块使SoC内的两个处理器能够通过MU
接口传递消息以进行通信和协调。
文章目录
- 1 MU特性
- 2 功能描述
- 3 MU通信实例
- 3.1 轮训实现多核通信
- 3.1.1 MU_SetFlags和MU_GetFlags
- 3.1.2 MU_SendMsg和MU_ReceiveMsg
- 3.1.3 调试从核注意事项
- 3.2 中断实现多核通信
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
为例进行分析,它实现以下功能:
- core 0通过MU模块以轮询模式向core 1发送消息。
- core 1通过轮询模式将消息回发给core 0。
- 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;
}
这里将GIRn
和Fn
的位都清零了,然后根据flag
的值再置Fn
的位:
可以看到GIRn
是用于中断通知MUA
的,这里我们用的是轮询方式,所以清零。对于Fn
位来说:
Fn
的3位分别代表MUB
向MUA
发送的不同标志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,且此时修改CR
的Fn
位也是无效的,我们需要等待其自动清零后才能置标志位。
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;
发送之前我们需要等待对应MU
的SR
寄存器的[23:20]位的TEn
(发送寄存器空)标志,四个位就对应四个通道。当消息发送到另一核后,该位会置0,当该位置1时,表示我们可以继续发送数据了。
- 上图为
MUA
寄存器的说明,MUB
类似
接着我们只要将数据写入TR
寄存器即可,四个通道各有一个32位的TR
寄存器:
来看一下MUA
中的TR0
寄存器的说明,TR1~TR3
类似:
- 写入
MUA
的TR0
寄存器的数据会反映在MUB
的RR0
中,这些寄存器都不是双缓冲的,所以数据会覆盖 - 写
TR0
会清除MUA
的SR
中的TE0
位,并置MUB
的SR
中的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
发来数据后,会置MUB
的SR
中的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_SendMsgNonBlocking
向MUB
发送消息,等这次发送完毕后,再次进入发送空中断则调用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
字节后,在下一次进入中断时关闭接收满中断。