如何在STM32H7双核设备上使用FreeRTOS消息缓冲区?

1.引言和理论

当在每个核心上运行FreeRTOS实例时,我们可以使用FreeRTOS消息缓冲区实现进程间通信(IPC),如下所述:
https://www.freertos.org/2020/02/simple-multicore-core-to-core-communication-using-freertos-message-...
实现的主要部分已经由FreeRTOS提供。我们只需要在核心之间实现两个通知:
  1. 缓冲区中有新消息时
  2. 读卡器释放消息缓冲区时
此外,我们应该为我们使用的每个消息缓冲区实现此通知。对于双向通信,我们需要为每个方向提供单独的消息缓冲区。
为了发送这些通知,我们可以使用硬件信号量(HSEM)外围设备。然而,这样我们就只能发送通知,而不能发送更多的数据。因此,我们需要4种不同的HW信号量来进行基本的双向通信。总共有32个,所以4个并不多。
或者,我们可以在传递这些信息的地方实现“控制”消息缓冲区。当我们想要在核心和不同任务之间实现多个消息缓冲区时,这变得非常有用。
请注意,每个缓冲区只能由单个任务写入。阅读也是如此。
在实现“控制”消息缓冲区的情况下,我们在消息中传递以下信息:
  1. 正在通知哪个消息缓冲区
  2. 通知是什么(新消息可用或消息已发布)
因此,当发送新的“数据”消息时,首先填充“数据”信息缓冲区,然后填充“控制”信息缓冲区时,最后生成HSEM通知。
当然,当“控制”消息缓冲区变满时可能会出现问题。然而,处理这种情况可能很困难,并且可能更容易使控制缓冲区足够大。为每个“数据”消息缓冲区留出2个事件的空间是一个很好的起点。与没有“控制”消息缓冲区的解决方案相比,这是一个缺点。
然而,我们可以检测到这个问题,并至少报告错误(而不是以一些未定义的行为结束)。“控制”消息缓冲区在HSEM中断处理程序中释放,因此对该中断具有高优先级也会有所帮助。
(理论上,你可以尝试单独为“控制”消息缓冲区实现一些通知,但你也需要确保只有一个线程在等待“控制”信息缓冲区。这种解决方案可能会带来更大的复杂性。)
我们还需要注意消息缓冲区的分配位置。最好的位置是将其放置在D3 SRAM中(起始0x3800000)。该内存可由两个内核访问,并且在其中一个内核进入低功耗模式时保留。由于我们需要分配多个句柄和缓冲区,最简单的方法是定义全局结构并将其放置在D3SRAM的开头。
在这里,我们假设CM4和CM7编译的代码将使用相同的大小和对齐结构。

2.示例实现

示例是为STM32H745发现板创建的,但它应该很容易移植到其他板/设备。
部分基于STM32Cube_FW_H7_V1.9.0固件包中提供的cm_ipc.c/.h(STM32Cube_FW_H7_1.9.0\Projects\STM32H747I-DISCO\Demolations\extra_modules\STIC)。
在这里,我们重点讨论使用“控制”消息缓冲区(FreeRTOS_MultipleMessageBuffers)的实现。这些示例还包含具有单个数据消息缓冲区的更简单版本。
在应用程序中,每个核心在通道0上周期性地发送消息。当核心接收到来自通道0的消息时,它将切换LED。CM4在接收到消息之后有意地添加延迟。此测试CM7线程正在等待消息缓冲区中的可用空间的情况
在通道1上,当按下板上的用户按钮时,CM4发送一条消息。当CM7接收到该消息时,它将切换其他LED。

2.1 STM32CubeMX配置

在STM32CubeMX中,我们需要为两个核心启用FreeRTOS中间件,CMSIS版本应该无关紧要,因为我们将直接调用FreeRTOS。此外,我们还需要为NVIC1(CM7)和NVIC2(CM4)中的两个内核启用HSEM中断。这个基本配置应该足够了。
在这个例子中,CM7和CM4的MPU都阻止访问除D3 SRAM之外的其他核心使用的FLASH和RAM存储器。这是为了避免和检测由错误配置引起的任何不必要的访问。

2.2添加cm_ipc.c/.h文件

您可以在所附的示例中找到cm_ipc.c/.h实现。
为了发送“控制”消息,我们使用以下结构:
typedef结构{MessageBufferHandle_t缓冲区;uint32_t is_receive;}amp_ctrl_msg_t;
对于缓冲区分配,我们定义了放置在D3 SRAM中的shared_ram_t结构:
typedef struct{MessageBufferHandle_t cm7_to_cm4_handle;MessageBufferHandle_t cm4_to_cm7_handle;StaticMessageBuffer_t cm7_to_cm4_xmsg;StaticMessage buffer_t cm4_to_cm 7_xmsg;uint32_t cm7-to_cm4_buffer[IPC_CHANNEL_buffer_SIZE/4];uint332_t cm4_to _cm7_buffer[IPC_CHANNEL_BUFBER_SIZE/4];}IPC_CHANNEL_t;typedef struct{/*控制消息缓冲区*/MessageBufferHandle_t cm7\o_cm4_handle;MessageBufferHandle_t cm4_to_cm7_handle;StaticMessageBuffer_t cm7_to_cm4_xmsg;StaticMessage buffer_t cm4_to_cm 7_xmmsg;uint32_t cm7-to_cm4_buffer[cm7_to_cm4_CTRL_SIZE/4];uint22_t cm4_to _cm7_buffer[cm4_to_cm7_CTRL_SIZE/4];ipc_channel_t channels[CIPC_NUMBER_OF_channels];}shared_rams _t;
在本例中,为了简化配置和初始化,所有“数据”缓冲区都具有相同的大小,因此我们可以简单地将它们定义为数组。
以下是从HSEM中断调用的中断处理程序,用于处理传入的“控制”消息:
static void prvCoreInterruptHandler(int ctrl){BaseType_t xHigherPriorityTaskWoken=pdFALSE;amp_ctrl_msg_t ctrl_msg;if(!xrx_ctrl_buf){return;}while(xMessageBufferReceiveFromISR(xrx_ctll_buf,&ctrl_mmsg,sizeof(amp_ctrll_msg_t),&xHigherPriorityTaskWoken)==sizeof(amp_ctrl_msg_t)){if(ctrl_msg.is_receive){xMessageBufferSendCompletedFromISR(ctrl_msg.buffer,&xHigher PriorityTaskwoken);}else{xMMessageBufferReceiveCompletedFromISR(ctrl-msg.bufffer,&xHigherPriorityTaskWoken,其中,xHigherPriorityTaskWoken初始化为pdFALSE,如果中断安全API取消阻止优先级高于当前执行任务优先级的任务,则将设置为pdTRUE。*/端口YIELD_FROM_ISR(xHigherPriorityTaskWoken);}

下面是用于发送事件的函数:
void vGenerateRemoteInterrupt(void*xUpdatedMessageBuffer,int is_receive){MessageBufferHandle_t xUpdatedBuffer=(MessageBufferHandle_t)xUpdatedMMessageBuffer;amp_ctrl_msg_t ctrl_msg;__DSB();if(xUpdatedBbuffer!=xtx_ctrl_buff和&xUpdatedBBuffer!=xrx_ctrl_Buff)(amp_ctrl_msg_t),0)!=sizeof(amp_ctrl_msg_t
在这里,我们可以检测“控制”消息缓冲区是否溢出,并调用一些错误处理程序。

2.3修改FreeRTOSConfig.h

在FreeRTOSConfig.h中,我们需要覆盖一些宏定义。在cm_ipc.C中还检查了额外的C宏,以确保正确完成此修改:
/*USER CODE BEGIN定义*//*可以添加参数定义的部分(例如,覆盖FreeRTOS.h中的默认定义)*/void vGenerateRemoteInterrupt(void*xUpdatedMessageBuffer,int is_receive)#define sbSEND_COMPLETE_FROM_ISR sbSEND_COMPLETED_FROM_ISR#define sbSEND_COMPLETED(pxStreamBuffer)vGenerateRemoteInterrupt(pxStreamBuffer,1)#define sbSEND_COMPLE TED_FROM_ISR(pxStream Buffer,pxHigherPriorityTaskWoken)vGenerateRemoteInterruptsbRECEIVE_COMPLETED_FROM_ISR(pxStreamBuffer,pxHigherPriorityTaskWoken)vGenerateRemoteInterrupt(pxStreamBuffer,0)#定义IPC_CHECK_sbSEND_COMPLETED/*用户代码END定义*/
注意:在FreeRTOS代码中使用sbSEND_COMPLETED_FROM_ISR而不是sbSEND_COMPLETED_FRAM_ISR时,似乎有轻微的拼写错误或命名不一致。
否则,FreeRTOS将尝试向本地线程传递通知,可能会发生一些令人讨厌的事情。(在某些情况下,它可能导致例如CM7执行CM4任务)。这就是为什么还有IPC_CHECK_sbSEND_COMPLETED宏,它检查那些FreeRTOS回调是否被覆盖。这在将cm_ipc.c/.h复制到新项目时非常有用。如果未定义宏,则cm_ipc.c将引发编译错误。

2.4修改链接脚本

修改linkerscript非常容易。对于这两个核心,我们需要确保修改以“_FLASH.ld”结尾的链接脚本,并在中断向量后添加“.shared_ram”部分:

.Irs_vector:{.=ALIGN(4);KEEP(*(.irs_vector))/*启动代码*/.=ALIGN(4));}>FLASH.shared_ram(NOLAD):{*(.shared_ram)}>ram_D3


对于Cortex-M4,我们需要定义D3 SRAM(RAM_D3区域):

内存{FLASH(rx):ORIGIN=0x08100000,LENGTH=1024K RAM(xrw):ORIGIN=0x10000000,LENGTH=288K RAM_D3(xrw

在STM32CubeIDE的窗口>显示视图>构建分析器中,我们可以检查数据是否位于0x3800000。
 

2.5初始化缓冲区和交换消息

在cm_ipc.c/.h文件中有函数ipc_init和ipc_start。ipc_init初始化消息缓冲区,ipc_start启用HSEM中断。应首先调用ipc_init,并在FreeRTOS已经运行时调用ipc_start。
在我们的示例中,缓冲区由CM7初始化,CM4在ipc_init执行后释放。
对于发送和接收数据,有ipc_send和ipc_receive函数。两者都将通道编号作为第一个参数。
在这两种情况下,我们都应该检查返回值,看看我们是否成功接收/发送了任何数据。在该示例中,这仅针对接收部分进行。

3.增加更多渠道,改进示例

当使用多个通道时,最好去掉“控制”消息缓冲区。这可以允许我们并行地发送来自中断和任务的消息,假设每个中断/任务使用单独的通道。
我们可以通过为每个双向通道专用更多的4个HSEM信号量来做到这一点,但我们很快就会用完可用的信号量。
我们可以通过使用存储在SRAM中的两个32位值来“模拟”HSEM通知事件。让我们把它们称为“remote_toggle”和“local_togle”。然后,每个事件在两个寄存器中都有其位,“local_toggle”由当前CPU修改,“remote_togle”由另一个CPU修改(因此,在另一个CPU上,这些寄存器被交换)。我们可以通过在“local_toggle”上切换一点来发送事件。在远程端,我们可以通过对“remote_toggle”和“local_togle”进行异或来检测传入事件。最后,另一个CPU通过切换“remote_toggle”来确认该事件。
当“切换”这些位时,我们应该确保只有一个线程/中断访问“local_toggle”。由于我们也希望从中断上下文发送消息,因此唯一安全的方法是全局禁用中断。然而,切换过程本身应该很短(3条指令,不包括中断的启用/禁用)。
在要求苛刻的实时应用程序中,我们可以通过优先级屏蔽全局禁用仅低优先级中断,。然而,在这种情况下,我们无法从那些未被禁用/屏蔽的中断中发送消息,或者我们需要为这些中断实现单独的通道(使用专用的HSEM信号量)。
这个机制在第三个例子中实现,并添加了额外的函数ipc_sendmsg_irq来发送来自中断例程的消息。
 

4.示例

软件包中有3个示例:
  1. FreeRTOS_MessageBuffer-核心之间仅实现一个双向缓冲区的简单示例
  2. FreeRTOS_MultipleMessageBuffers-使用多个消息缓冲区实现“控制”消息缓冲区的高级示例
  3. FreeRTOS_MultipleMessageBuffers2-使用“切换”机制从中断例程发送消息的改进示例
所有示例都是为STM32H745 Discovery板制作的,但它们可以很容易地移植到其他板上。
在此处下载示例