基于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>© 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)。

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

在上面的资源分配图中,除了使用颜色标记不同核以外,还标记了各个外设可以由哪些核访问。如果区分颜色的是斜线则表示共享,即同一时刻可以由多个核访问该外设;如果区分颜色的是竖线则表示独占,即同一时刻只能由其中一个核访问该外设。而只有一种颜色的表示只能由该颜色代表的核访问。图中还展示了用于管理协处理器M4的核间通信控制模块(IPCC)和硬件信号量(HSEM),它们只能由A7的普通核(非安全)与M4核来使用。它们提供中断信号,允许处理器以非阻塞方式交换信息,是异核通信的基础。
所谓多核异构(AMP)是指芯片内拥有不同类型的核(比如A7和M4),如果是拥有多个同样的核,称为多核同构(SMP)。显然多核异构要复杂,所以必须规定异核间的通信机制。一般来说,使用性能较高的核(如A7)处理一些高性能的任务,比如网络通信、图形显示、人机交互界面等应用。而使用协核(如M4)处理一些实时性较高的任务,比如传感器读写、电机驱动、系统监控等应用。
异核间的通信本质上是依赖共享内存进行的,下面给出的是STM32MP157的内存地址映射图。

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

这部分可根据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区域,各自配置各自“看得到”的区域,两者配置的大小要完全一样,因为它们就是同一块物理内存。下图是按照官方默认配置的分配情况。

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

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

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

从表中可以看到,通道1和通道2为全双工模式,用于在A7和M4间相互通知并处理消息。注意,这里只是处理消息通知,真正的数据在共享内存中。通道3为单工模式,用于A7远程控制M4的生命周期。表中也标记了这些通道使用的各自核的框架。
在上图中,IPCC在A7端的Linux内核中被封装成了一个Mailbox(邮箱框架),在软件层面可以通过它来查询是否有消息要读写。Mailbox的具体结构如下图所示。

关于邮箱框架的具体技术原理可参考官方的资料,这里就不赘述了。综上所述,A7和M4之间通过共享内存交换数据,通过核间中断通知消息。
下面简要介绍一下OpenAMP框架,它是开放非对称多处理的英文缩写。最初由Mentor Graphics与Xilinx公司为解决AMP系统中基于RTOS(或祼机程序)与Linux接口能进行通信而开发的一个软件框架。它为AMP系统应用开发提供了三个重要的组件,即Virtio、RPMsg和Remoteproc。目前,OpenAMP可以使用在ST、NXP、TI以及Xilinx等平台上,厂商已经提供了移植好的实例,在开发时可以进行参考。
先看第1个组件Virtio,即虚拟化组件,具体结构如下图所示。

前面说过,通过共享内存进行异核间通信是必要的,但在共享内存中如何组织数据,如何决定先后顺序,何时收何时发等等问题都需要解决才行。为此OpenAMP框架提供了一个虚拟化模块Virtio。在其中有两个vring,它们是指向数据缓冲区的FIFO队列,一个用于接收一个用于发送,两个vring就构成了一个收发的循环,收发的数据被存放在各自的Vring Buffers(即共享内存)中。
接下来看第2个组件RPMsg,即Remote Processor Messaging。它用于远程处理器的消息传递,具体结构如下图所示。

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

在上图中有一个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相关的文件,如下表所示。

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

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

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

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

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

浙公网安备 33010602011771号