基于STM32MP157的异核间通信(续一)

前面讨论了基于RPMsg框架进行异核间通信的方式,虽然操作过程有些繁琐,但它却是核间通信最基本的方式。为了进一步简化操作,可以对其进行进一步封装,把核间通信的通道直接映射成基于tty的虚拟串口,这样无论是在A7端还是在M4端,就都可以使用操作串口的方式来实现异核间通信。下面就来讨论一下这种方式。

仍然使用前面的例子,通过A7向M4发送字符串”led_on“和”led_off“来实现LED的亮灭控制。同时,M4也向A7发送LED亮灭信息。由于在野火STM32MP157的Linux内核中已经包含了虚拟串口模块,所以只需要编写M4端程序就可以了。这里仍然使用AMP_test_CM4的工程,下面给出整个main.c文件内容(包含模板内容)。

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2026 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "ipcc.h"
#include "openamp.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "virt_uart.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
#define LED_ON        "led_on"            //匹配亮灯字符串命令
#define LED_OFF        "led_off"            //匹配熄灯字符串命令
#define MAX_BUFFER_SIZE    RPMSG_BUFFER_SIZE

uint8_t Copy_Buffer[MAX_BUFFER_SIZE];    //接收缓冲区
uint8_t BuffTx[MAX_BUFFER_SIZE];        //发送缓冲区

VIRT_UART_HandleTypeDef    huart0;            //虚拟串口句柄
uint16_t RxSize = 0;                    //接收大小
__IO FlagStatus flag = RESET;            //接收标志
//以下申明接收回调函数
void VIRT_UART0_RxCpltCallback(VIRT_UART_HandleTypeDef *huart);
//以下重定位USART3串口到printf函数
#ifdef __GNUC__
#define    PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define    PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
    while ((USART3->ISR & 0x40) == 0);
    USART3->TDR = (uint8_t)ch;
    return ch;
}
/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  if(IS_ENGINEERING_BOOT_MODE())
  {
    /* Configure the system clock */
    SystemClock_Config();
  }
  else
  {
    /* IPCC initialisation */
     MX_IPCC_Init();
    /* OpenAmp initialisation ---------------------------------*/
    MX_OPENAMP_Init(RPMSG_REMOTE, NULL);
  }

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART3_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("**********Start Initialize Virtual UART0**********\r\n");
  if (VIRT_UART_Init(&huart0) != VIRT_UART_OK)
  {
      printf("*******VIRT_UART_Init UART0 failed.*******\r\n");
      Error_Handler();
  }
  if (VIRT_UART_RegisterCallback(&huart0, VIRT_UART_RXCPLT_CB_ID, VIRT_UART0_RxCpltCallback) != VIRT_UART_OK)
      Error_Handler();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
      OPENAMP_check_for_message();        //调用邮箱中断检测函数
      if(flag == SET)                    //若接收到信息
      {
          flag = RESET;                    //接收标志复位
          if (!strncmp((char *)Copy_Buffer, LED_ON, strlen(LED_ON)))    //匹配亮灯字符串
          {
              strcpy((char *)BuffTx, "M4: LED_ON\n");        //拷贝亮灯字符串到发送缓冲区
              VIRT_UART_Transmit(&huart0, BuffTx, strlen((const char *)BuffTx));    //发送亮灯字符串
              HAL_GPIO_WritePin(GPIOA, GPIO_PIN_13, GPIO_PIN_RESET);    //点亮LED
          }
          if (!strncmp((char *)Copy_Buffer, LED_OFF, strlen(LED_OFF)))    //匹配熄灯字符串
          {
              strcpy((char *)BuffTx, "M4: LED_OFF\n");        //拷贝熄灯字符串到发送缓冲区
              VIRT_UART_Transmit(&huart0, BuffTx, strlen((const char *)BuffTx));    //发送熄灯字符串
              HAL_GPIO_WritePin(GPIOA, GPIO_PIN_13, GPIO_PIN_SET);        //熄灭LED
          }
          memset(Copy_Buffer, 0, RxSize);                    //清空接收缓冲区
          memset(BuffTx, 0, strlen((const char *)BuffTx));    //清空发送缓冲区
      }
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_LSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = 16;
  RCC_OscInitStruct.HSIDivValue = RCC_HSI_DIV1;
  RCC_OscInitStruct.LSIState = RCC_LSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  RCC_OscInitStruct.PLL2.PLLState = RCC_PLL_NONE;
  RCC_OscInitStruct.PLL3.PLLState = RCC_PLL_NONE;
  RCC_OscInitStruct.PLL4.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** RCC Clock Config
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_ACLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
                              |RCC_CLOCKTYPE_PCLK3|RCC_CLOCKTYPE_PCLK4
                              |RCC_CLOCKTYPE_PCLK5;
  RCC_ClkInitStruct.AXISSInit.AXI_Clock = RCC_AXISSOURCE_HSI;
  RCC_ClkInitStruct.AXISSInit.AXI_Div = RCC_AXI_DIV1;
  RCC_ClkInitStruct.MCUInit.MCU_Clock = RCC_MCUSSOURCE_HSI;
  RCC_ClkInitStruct.MCUInit.MCU_Div = RCC_MCU_DIV1;
  RCC_ClkInitStruct.APB4_Div = RCC_APB4_DIV1;
  RCC_ClkInitStruct.APB5_Div = RCC_APB5_DIV1;
  RCC_ClkInitStruct.APB1_Div = RCC_APB1_DIV1;
  RCC_ClkInitStruct.APB2_Div = RCC_APB2_DIV1;
  RCC_ClkInitStruct.APB3_Div = RCC_APB3_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
//以下为接收回调函数
void VIRT_UART0_RxCpltCallback(VIRT_UART_HandleTypeDef *huart)
{
    //打印接收到的信息(通过USART3口)
    printf("\r\nReceived on Virtual UART0:\n\r%s\n\rSize:%d\n\r", (char *)huart->pRxBuffPtr, huart->RxXferSize);
    //接收到的数据大小
    RxSize = (huart->RxXferSize < MAX_BUFFER_SIZE) ? huart->RxXferSize : (MAX_BUFFER_SIZE - 1);
    memcpy(Copy_Buffer, huart->pRxBuffPtr, RxSize);        //拷贝接收到的字符串到接收缓冲区
    flag = SET;        //接收标志置位
}
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

同前面一样,把编译后生成的AMP_test_CM4.elf文件拷贝到开发板目录/lib/firmware下,然后执行命令”echo AMP_test_CM4.elf > /sys/class/remoteproc/remoteproc0/firmware“把文件加载给M4,接着执行命令”echo start > /sys/class/remoteproc/remoteproc0/state“启动执行,可看到打印的相关信息。查看一下/dev目录下,可以看到已经有一个名为ttyRPMSG0的虚拟串口设备节点了。接下来进行测试,执行命令“echo "led_on" > /dev/ttyRPMSG0”即可点亮LED,执行“echo "led_off" > /dev/ttyRPMSG0”即可熄灭LED,说明基于STM32MP157系统的A7与M4异核间虚拟串口通信完全成功。还可以使用前面“基于STM32MP157的异核间通信”中的app进行验证,只需要更改一下设备节点名称为/dev/ttyRPMSG0即可,这里就不赘述了。

通过上述测试可以看出,由于Linux内核已经具备了虚拟串口驱动,在开发上就不需要编写内核驱动了,这可大大减少了开发工作量。但由于虚拟串口是经过了再次封装,所以其通信效率及通信延时都不及RPMsg方式。

在上述讨论中,都是通过手动加载M4固件的形式来进行测试的,这不太适合程序的最终运作,下面来看一下如何把M4固件设置成自动加载的模式。自动加载有很多种方式,这里讨论通过systemctrl的方式来进行自动加载。首先需要编写一个可执行的脚本文件,把它命名为start_M4.sh,其内容如下。

#!/bin/sh
cd /lib/firmware/
echo AMP_test_CM4.elf > /sys/class/remoteproc/remoteproc0/firmware
echo start > /sys/class/remoteproc/remoteproc0/state

以上脚本文件可以放置在任何目录下,比如/opt目录下,并把它加上可执行权限。接着进入到/lib/systemd/system目录下,编写一个service脚本,命名为start_m4.service,内容如下。

[Unit]
Description=start M4
After=multi-user.target

[Service]
Type=simple
User=root
ExecStart=/opt/start_M4.sh

[Install]
WantedBy=multi-user.target

最后执行命令“systemctl enable start_m4.service”加载该服务脚本,成功后会显示“Created symlink /etc/systemd/system/multi-user.target.wants/start_m4.service → /lib/systemd/system/start_m4.service.”。实际上就是在multi-user.target.wants下创建一个系统链接,这样在系统启动时就可以自动运行该项服务了。若要取消,可把上述命令中的enable换成disable就可以了。

以上成功测试了基于STM32MP157的异核间通信,下面简要讨论一下OpenAMP和RPMsg软件框架的原理。

在STM32MP157芯片中,除了一个双核的A7和一个M4外,还有很多外设提供各个CPU使用,具体的资源分配如下图所示(来自官网STM32MP15 peripherals overview - stm32mpu)。

STM32MP1IPsOverview

上图中,Cortex-A7是主处理器(master),拥有两个核,一个具体安全模式(红色),另一个是普通核(黑色)。Cortex-M4是协处理器(co-processor),只有一个核(蓝色)。在开发板上的显示如下图所示。

111

在上面的资源分配图中,除了使用颜色标记不同核以外,还标记了各个外设可以由哪些核访问。如果区分颜色的是斜线则表示共享,即同一时刻可以由多个核访问该外设;如果区分颜色的是竖线则表示独占,即同一时刻只能由其中一个核访问该外设。而只有一种颜色的表示只能由该颜色代表的核访问。图中还展示了用于管理协处理器M4的核间通信控制模块(IPCC)和硬件信号量(HSEM),它们只能由A7的普通核(非安全)与M4核来使用。它们提供中断信号,允许处理器以非阻塞方式交换信息,是异核通信的基础。

所谓多核异构(AMP)是指芯片内拥有不同类型的核(比如A7和M4),如果是拥有多个同样的核,称为多核同构(SMP)。显然多核异构要复杂,所以必须规定异核间的通信机制。一般来说,使用性能较高的核(如A7)处理一些高性能的任务,比如网络通信、图形显示、人机交互界面等应用。而使用协核(如M4)处理一些实时性较高的任务,比如传感器读写、电机驱动、系统监控等应用。

异核间的通信本质上是依赖共享内存进行的,下面给出的是STM32MP157的内存地址映射图。

333

从图中可以看出,RAM区域被划分为4块,其中第1部分BOOT区主要用于放置中断向量入口地址(M4的RETRAM),这部分一般不动它。第2部分是SRAMs区,它又被划分成了4个区域,具体如下表所示。

222

这部分可根据M4端的链接脚本和A7端的设备树进行配置,以上是按照ST公司官方默认的配置。从表中可以看到,SRAM1~SRAM4的地址空间是连续的,范围是0x10000000~0x1005FFFF,总大小为384KB。若不使用A7核,那么M4核可以全部使用这384KB的内存来运行程序,M4核所依赖的RAM和ROM都在这个范围内。第3部分是SYSRAM区,它是A7内核下的系统内存区域。第4部分是RAM aliases区,它是A7内核下的地址别名区,从上图中可以看到,它映射出来的大小与SRAMs区和BOOT区是完全一样的。也就是说,M4能“看到”的SRAMs区(包含RETRAM64KB)与A7能“看到”的RAM aliases区(包含RETRAM64KB)实际上是同一片物理内存。之所以要这样从地址上区分,是为了配置上的方便与安全。也就是M4端通过链接脚本配置SRAMs区域,A7端通过设备树配置RAM aliases区域,各自配置各自“看得到”的区域,两者配置的大小要完全一样,因为它们就是同一块物理内存。下图是按照官方默认配置的分配情况。

111

从图中可见,默认配置下(注:可以更改),M4没有使用SRAM4部分,它被分配给了A7的普通核做DMA缓冲区用了。而SRAM3部分是A7和M4都共享的,这部分可以做异核间的通信使用。其实芯片内异核间的通信就是利用共享内存实现的,如下图所示。

1553657-20260811220703454-461013395

从上图中可以看出,A7和M4有各自的内存和外设,而基于AMP结构的通信就是在各自的内存中划出一块来进行共享,利用这块共享内存(Shared SRAM)实现信息交换。有了共享内存还不够,还需要配套相应的消息机制,以便通知各自的核何时利用共享内存来读(或写)数据。配套的机制有很多种,其中最主要的就是IPCC(Inter-Process Communication controller),具体如下图所示。

222

IPCC属于硬件部分,它有6个双向通道,用于在两个CPU之间进行信号交换。从上图中可以看出,IPCC依赖于各自的CPU中断,如在A7端使用的是GIC,在M4端使用的是NVIC。IPCC的6个通道具体如下表所示。

111

从表中可以看到,通道1和通道2为全双工模式,用于在A7和M4间相互通知并处理消息。注意,这里只是处理消息通知,真正的数据在共享内存中。通道3为单工模式,用于A7远程控制M4的生命周期。表中也标记了这些通道使用的各自核的框架。

在上图中,IPCC在A7端的Linux内核中被封装成了一个Mailbox(邮箱框架),在软件层面可以通过它来查询是否有消息要读写。Mailbox的具体结构如下图所示。

111

关于邮箱框架的具体技术原理可参考官方的资料,这里就不赘述了。综上所述,A7和M4之间通过共享内存交换数据,通过核间中断通知消息。

下面简要介绍一下OpenAMP框架,它是开放非对称多处理的英文缩写。最初由Mentor Graphics与Xilinx公司为解决AMP系统中基于RTOS(或祼机程序)与Linux接口能进行通信而开发的一个软件框架。它为AMP系统应用开发提供了三个重要的组件,即Virtio、RPMsg和Remoteproc。目前,OpenAMP可以使用在ST、NXP、TI以及Xilinx等平台上,厂商已经提供了移植好的实例,在开发时可以进行参考。

先看第1个组件Virtio,即虚拟化组件,具体结构如下图所示。

333

前面说过,通过共享内存进行异核间通信是必要的,但在共享内存中如何组织数据,如何决定先后顺序,何时收何时发等等问题都需要解决才行。为此OpenAMP框架提供了一个虚拟化模块Virtio。在其中有两个vring,它们是指向数据缓冲区的FIFO队列,一个用于接收一个用于发送,两个vring就构成了一个收发的循环,收发的数据被存放在各自的Vring Buffers(即共享内存)中。

接下来看第2个组件RPMsg,即Remote Processor Messaging。它用于远程处理器的消息传递,具体结构如下图所示。

222

从上图中可以看出,RPMsg框架是位于虚拟化模块Virtio之上的,也就是说,RPMsg是基于Virtio之上的一个抽象。当A7端有数据装入Vring buffers后,会通过邮箱中断来通知M4,M4端就可以从Vring buffers中读取数据,反之亦然。若以网络的层级结构来类比一下,则共享内存就相当于物理层,提供最低层的硬件支持;Virtio相当于MAC层,提供数据的组织结构;RPMsg相当于传输层,提供数据的消息总线。

最后看第3个组件Remoteproc,即远程处理组件,具体结构如下图所示。

111

在上图中有一个stm32_rproc,它是M4的内核驱动,其作用有三:一是向Remoteproc框架注册特定功能,比如回调函数等;二是处理与M4关联的平台资源,比如寄存器、存储器、看门狗、时钟及复位等;三是通过邮箱框架把通知转发到M4。这里要注意,M4对于Linux来说也是一个远程外设,所以需要驱动程序。Remoteproc框架的作用主要有以下几点:一是A7端把M4的固件加载到M4的内存中去执行;二是解析M4固件资源表并设置关联的资源;三是控制M4内核的启动与关闭;四是为A7和M4核间通信建立一个RPMsg通道;五是提供监视和调试服务,在Linux下通过sysfs和debugfs两个文件系统的支持,提供了监视和调试功能。

基于RemoteProc和RPMsg的通信过程可总结如下:

1、A7核启动Linux系统;
2、A7端通过Remoteproc加载M4固件;
3、A7端解析M4固件资源表,并创建Virtio;
4、A7端启动协处理器;
5、A7端和M4端之间创建RPMsg驱动;
6、A7端给M4端发送第一条消息,用于激活RPMsg通道;
7、A7端与M4端相互发送数据。

以上是OpenAMP组件的简要介绍,若想进一步了解可查询官方文档。在Linux内核源码drivers/mailbox目录下有Mailbox和IPCC相关的文件,如下表所示。

111

在Linux内核源码drivers/virtio目录下有Virtio相关的文件,如下表所示。

1553657-20260812134733691-1776583837

在Linux内核源码drivers/rpmsg目录下有RPMsg相关的文件,如下表所示。

1553657-20260812134308391-566186902

在Linux内核源码drivers/remoteproc目录下有Remoteproc相关的文件,如下表所示。

111

此外,在M4端的OpenAMP库下也提供了相关驱动文件,如下表所示。

111

最后说明一点,在前面提供的实例中,并没有完全使用邮箱中断,而是在M4端的程序中,循环调用OPENAMP_check_for_message()函数来实现查询。具体原因是考虑到如果在中断服务程序中操作不当,会引发一序列的问题。OpenAMP毕竟只是一个软件框架,不是单纯的中断驱动。它需要管理端点(endpoint)、维护队列、处理各种协议状态,这些逻辑比较复杂,不适合全部放在中断里执行。如果希望使用中断方式,可自行研究官方提供的例程。

-待续-

posted @ 2026-08-12 15:04  fxzq  阅读(27)  评论(0)    收藏  举报