简单介绍基于STM32MP157的Remoteproc和RPMsg框架。
STM32MP1系列产品,是STM32进军Linux的首款微处理器,采用MCU+MPU的组合,集成两颗主频为650MHz的Cortex-A7应用处理器内核和一颗主频为209MHz的Cortex-M4微控制器内核。
非对称多处理Asymmetric Multiprocessing(AMP)虽然目前在嵌入式还不是主流,但未来肯定是趋势。将多媒体处理扔给专用的MCU,亦或将对控制延时敏感的传感器交给MCU实时控制,更多的组合给人更多的遐想。
对于非对称多核架构,不同的核心是如何启动运行,又是如何进行通信?这些疑惑在上手STM32MP157后,逐渐明朗,因此记录下笔记。
1.生成M4固件
在进行启动M4之前,需要先建立工程,生成M4固件,这里以点灯为例,简单说下创建STM32MP157的M4工程。
这里要M4点灯,涉及到资源的分配,资源分配如下图所示。

深蓝色的IP为A7独占,浅蓝色的IP为M4独占,竖线分割的IP为同一时刻只能一个占有,斜线分割的IP为任意时刻两者可以同时占用。
比如这里GPIO就为两者可以同时占用,在Linux中可以控制GPIO,同时M4也可控制GPIO。UART则为只能一个独占,分配给M4后,A7将不能控制。
支持STM32开发的集成开发环境有很多,国内熟知的有
Keil MDK-ARM
和IAR EWAR
。这两个IDE都很好用,但它们都是商业软件,免费或评估版要么有器件型号限制,要么有程序容量限制。于是出现了免费的Eclipse+GNU GCC来搭建STM32开发环境,但搭建过程繁琐、版本差异大导致教程不统一,对新手很不友好。STM32CubeIDE是ST公司基于Eclipse/CDT框架和GUN GCC工具链制作的免费IDE,并集成了STM32CubeMX。一个软件就可以实现STM32系列芯片的外围设备配置、代码生成、代码编辑、代码编译、在线调试,并且支持数百个Eclipse现有插件。
打开STM32CubeIDE,创建一个新的“STM32 Project”。

在弹出的STM32CubeMX,选择“STM32MP157A”,具体型号以自己使用的开发板为准。注意这里的“芯片资料区”,提供了该型号的芯片手册,不用再去网上找了。

再设置工程名字,打开STM32CubeMX的关联视图。

我使用的板子,LED灯接在PD13引脚上,因此这里把PD13设置为输出引脚。

需要注意,这里还要选中该引脚,右键弹出“Pin Reservation”,选择“Cortex-M4”,不然不会自动生成GPIO初始化代码。

最后,如图设置下GPIO的属性。

设置完后,在标签栏选择“Project”->“Generate Code”,即可自动生成相关初始化代码。默认的初始化代码如下图,需要注意的是“main.c”文件,在里面添加LED灯的控制逻辑。还有“stm32mp1xx_hal_gpio.c”,这个是hal库源码,从里面可知hal提供的GPIO相关操作函数,比如这里用到的HAL_GPIO_WritePin()
。

添加完LED的控制逻辑代码后,在标签栏选择“Project”->“Build Project”即可编译工程,得到GPIO_LED_CM4.elf
。该文件就是M4的固件,包含Cortex-A7和Cortex-M4都可以访问的资源表(.resource_table
)和LED的控制程序等。

在Linux里,使用readelf -a GPIO_LED_CM4.elf
命令,可以获取ELF文件的更多信息。
2.Remoteproc框架
Remoteproc(Remote Processor Framework
),主要作用就是对远程处理器的生命周期进行管理,即启动、停止远程处理器。
以STM32MP157为例,Cortex-A内核先启动,然后使用Linux RemoteProc框架进行加载Cortex-M4固件,启动M4内核。

ST官方提供的内核已经默认配置了Remoteproc驱动,进入系统后,首先将要运行的M4固件放在/lib/firmware/
目录下,然后将固件名字写到/sys/class/remoteproc/remoteproc0/firmware
,再操作/sys/class/remoteproc/remoteproc0/state
启动、停止M4处理器。
除了在Linux的用户态控制M4内核的生命周期,还能在Linux内核态使用API控制(参考linux-origin_master/Documentation/remoteproc.txt
),甚至U-boot中控制。
3.RPMsg框架
Remoteproc框架实现了对远程处理器生命周期的管理,RPMsg框架(Remote Processor Messaging Framework
)则是实现对远程处理器信息传递。
RPMsg是基于VirtIO的消息总线,它允许内核驱动程序与系统上可用的远程处理器进行通信,同时,驱动程序可以根据需要公开适当的用户空间接口(参考linux-origin_master/Documentation/rpmsg.txt
)。
STM32MP1多核通信框架如下图。

消息服务基于共享内存,使用RPMsg
和Virtio
框架,RemoteProc
框架则控制远程处理器生命周期。
信号通知(Mailbox
)服务则基于内部IPCC(Inter-Processor communication controller
),ST提供OpenAMP
相关库。
这里列举两个示例:
第一个示例在Linux的用户态和M4通信,实现A7控制M4的灯,A7和M4的相互唤醒;
第二个示例则是在Linux的内核态创建一个简单的RPMsg客户端,实现A7和M4的大量数据传输。
3.1 用户态的通信
3.1.1 A7准备
ST官方提供的内核已经默认配置了RPMSG_TTY驱动,Linux这边就不需要做什么了。
STM32MP1多核消息通信应用接口框图如下,在RPMsg
和Virtio
框架创建一个面向用户态的/dev/ttyRPMSG
接口,M4在OpenAMP
上创建虚拟串口,两者最终效果像是串口透传。

3.1.2 M4准备
创建一个STM32工程,在STM32CubeMX里,依次配置GPIO用于LED、配置UART5用于M4打印、以及配置IPCC和OPENAMP用于通信。

注意配置IPCC时,需要在NVIC Settings
选项卡里,将IPCC RX1 occupied interrupt
和IPCC TX1 free interrupt
的使能勾选上,不然后面的OPENAMP的Activated
始终为灰色,无法激活。
生成初始化代码后,在USER CODE BEGIN 0
和USER CODE END 0
之间添加printf
的重定向函数,让UART5与printf
绑定。
这里计划创建两个RPMsg tty通道,一个用来LED控制命令,一个用来传输唤醒命令。
- 1.初始化两个
RPMsg tty
虚拟串口
|
|
- 2.注册回调函数以按通道接收消息123456789if(VIRT_UART_RegisterCallback(&huart0, VIRT_UART_RXCPLT_CB_ID, VIRT_UART0_RxCpltCallback) != VIRT_UART_OK){Error_Handler();}if(VIRT_UART_RegisterCallback(&huart1, VIRT_UART_RXCPLT_CB_ID, VIRT_UART1_RxCpltCallback) != VIRT_UART_OK){Error_Handler();}
- 3.编写虚拟串口回调函数
当RPMsg收到数据后,将调用该回调函数。在此函数里,需要将接收的数据复制到用户内存,并修改接收标志位,通知用户完成数据接收。123456789101112131415161718192021/* USER CODE BEGIN 4 */void VIRT_UART0_RxCpltCallback(VIRT_UART_HandleTypeDef *huart){printf("Msg received on VIRTUAL UART0 channel: %s \n\r", (char *) huart->pRxBuffPtr);/* copy received msg in a variable to sent it back to master processor in main infinite loop*/VirtUart0ChannelRxSize = huart->RxXferSize < MAX_BUFFER_SIZE? huart->RxXferSize : MAX_BUFFER_SIZE-1;memcpy(VirtUart0ChannelBuffRx, huart->pRxBuffPtr, VirtUart0ChannelRxSize);VirtUart0RxMsg = SET;}void VIRT_UART1_RxCpltCallback(VIRT_UART_HandleTypeDef *huart){printf("Msg received on VIRTUAL UART1 channel: %s \n\r", (char *) huart->pRxBuffPtr);/* copy received msg in a variable to sent it back to master processor in main infinite loop*/VirtUart1ChannelRxSize = huart->RxXferSize < MAX_BUFFER_SIZE? huart->RxXferSize : MAX_BUFFER_SIZE-1;memcpy(VirtUart1ChannelBuffRx, huart->pRxBuffPtr, VirtUart1ChannelRxSize);VirtUart1RxMsg = SET;}/* USER CODE END 4 */
4.主函数轮询RPMsg消息
OPENAMP_check_for_message()
查询MailBox状态。
当收到数据时,VIRT_UARTx_RxCpltCallback()
会保存好收到数据,然后修改VirtUartxRxMsg
标志位。
主函数里发现VirtUartxRxMsg
标志位发生变化时,即可获取接收的数据。1234567891011121314151617181920while (1){OPENAMP_check_for_message();/* USER CODE END WHILE *//* USER CODE BEGIN 3 */if (VirtUart0RxMsg){VirtUart0RxMsg = RESET;/*VirUART0收到数据*/}if (VirtUart1RxMsg){VirtUart1RxMsg = RESET;/*VirUART1收到数据*/}}5.VirUART0接收控制LED指令
每次VirtUart0RxMsg
发生变化,说明VirUART0收到了数据。
然后比较收到的数据内容,执行对应的操作。
这里,M4收到MSG_LED_ON
(*led_on
)则打开LED灯,并发送消息给A7;M4收到MSG_LED_OFF
(*led_off
)则关闭LED灯,并发送消息给A7。12345678910111213141516171819202122if (VirtUart0RxMsg){VirtUart0RxMsg = RESET;if (!strncmp((char *)VirtUart0ChannelBuffRx, MSG_LED_ON, strlen(MSG_LED_ON))){strcpy((char *)BuffTx, "m4:led on\n");printf("%s\r", BuffTx);VIRT_UART_Transmit(&huart0, BuffTx, strlen((const char *)BuffTx));HAL_GPIO_WritePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin, GPIO_PIN_RESET);}if (!strncmp((char *)VirtUart0ChannelBuffRx, MSG_LED_OFF, strlen(MSG_LED_OFF))){strcpy((char *)BuffTx, "m4:led off\n");printf("%s\r", BuffTx);VIRT_UART_Transmit(&huart0, BuffTx, strlen((const char *)BuffTx));HAL_GPIO_WritePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin, GPIO_PIN_SET);}memset(VirtUart0ChannelBuffRx, 0 ,VirtUart0ChannelRxSize);memset(BuffTx, 0 ,strlen((const char *)BuffTx));}6.VirUART1接收休眠唤醒指令
每次VirtUart1RxMsg
发生变化,说明VirUART1收到了数据。
然后比较收到的数据内容,执行对应的操作。
这里,M4收到MSG_STOP
(*stop
)则进入CStop
模式,中途A7再发任意数据给M4,由于IPCC也可设置为中断唤醒源,将M4唤醒;M4收到MSG_DELAY
(*delay
)则等待20S后发数据给A7,在这20S内,将A7先休眠,随后将被M4唤醒。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465if (VirtUart1RxMsg){VirtUart1RxMsg = RESET;if (!strncmp((char *)VirtUart1ChannelBuffRx, MSG_STOP, strlen(MSG_STOP))){strcpy((char *)BuffTx, "m4:stop\n");printf("%s\r", BuffTx);VIRT_UART_Transmit(&huart1, BuffTx, strlen((const char *)BuffTx));//RCC_backupClocks();/* Clear the MCU flags before going into CSTOP */SET_BIT(PWR->MCUCR, PWR_MCUCR_CSSF);printf("Going to CStop mode\r\n");/* (C)STOP protection mechanism* Only the IT with the highest priority (0 value) can interrupt.* RCC_WAKEUP_IRQn IT is intended to have the highest priority and to be the* only one IT having this value* RCC_WAKEUP_IRQn is generated only when RCC is completely resumed from* CSTOP */__set_BASEPRI(1 << (8 - __NVIC_PRIO_BITS));HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON, PWR_STOPENTRY_WFI);/* To allow Systick to increment after CSTOP (Eg.: to not block during* TIMEOUT routines), TICK_INT_PRIORITY < BASEPRI* For this example as TICK_INT_PRIORITY = 1, BASEPRI should be 2 */__set_BASEPRI(2 << (8 - __NVIC_PRIO_BITS));printf("Leaving CStop mode\r\n");/* Test if system was on STOP mode */if( (PWR->MCUCR & PWR_MCUCR_STOPF) == PWR_MCUCR_STOPF){printf("System was on STOP mode\r\n");/* Clear the MCU flags */SET_BIT(PWR->MCUCR, PWR_MCUCR_CSSF);/* Restore clocks *//*if (RCC_restoreClocks() == HAL_OK){printf("CM4 restored clocks successfully\r\n");}*/}/* All level of ITs can interrupt */__set_BASEPRI(0U);}if (!strncmp((char *)VirtUart1ChannelBuffRx, MSG_DELAY, strlen(MSG_DELAY))){printf("Waiting 20 secs before sending the answer message\r\n");HAL_Delay(20 *1000);strcpy((char *)BuffTx, "m4:wakeup A7\n");printf("%s\r", BuffTx);VIRT_UART_Transmit(&huart1, BuffTx, strlen((const char *)BuffTx));}memset(VirtUart1ChannelBuffRx, 0 ,VirtUart1ChannelRxSize);memset(BuffTx, 0 ,strlen((const char *)BuffTx));}
为了A7能发消息将M4唤醒,还需要IPCC作为M4的中断唤醒源。
3.1.3 M4完整代码
|
|
3.1.4 测试效果
- 1.测试A7命令M4控制LED灯
首先将前面编译生成的rpmsg_user_CM4.elf
放在/lib/firmware目录下
。
然后启动固件,此时看到打印信息里提示创建了两个通道ttyRPMSG0
和ttyRPMSG1
,同时M4也打印了测试开始信息。123cd /sys/class/remoteproc/remoteproc0echo rpmsg_user_CM4.elf > firmwareecho start > state
然后还需要设置虚拟串口/dev/ttyRPMSG0
。-onlcr
是不将NL
字符映射为CR-NL
字符,就是说发送给M4的数据,不会自动加上回车,不然这里发送led_on
,M4收到的为led_on\n\r
,正确的应该是收到*led_on\r
。-echo
是禁止回显,以方便查看接收的字符。
最后,向/dev/ttyRPMSG0
写入预定义的指令,M4收到指令便会控制LED灯,并发送结果给A7。

- 2.测试A7和M4休眠唤醒
接着前面,启动M4后。
首先设置虚拟串口/dev/ttyRPMSG1
。12stty -onlcr -echo -F /dev/ttyRPMSG1cat /dev/ttyRPMSG1 &
然后向/dev/ttyRPMSG1
写入*stop
,M4随后卡在打印Going to CStop mode
后,接着向/dev/ttyRPMSG1
写入wakeup
(任意字符即可),M4随后打印Leaving CStop mode
。即实现了A7控制M4的休眠和唤醒。
再测试M4唤醒A7。
先使能A7唤醒,然后向/dev/ttyRPMSG1
写入*delay
,M4收到指令后,20S后会向A7发数据,从而唤醒A7。
此时控制A7进入休眠状态,20S后,A7被唤醒。

3.2 内核态的通信
3.2.1 A7准备
内核已经提供了示例驱动程序linux-origin_master/samples/rpmsg/rpmsg_client_sample.c
,这里直接使用该驱动,简单的修改了下打印内容,方便查看。
驱动比较简单,核心是rpmsg_sample_cb()
,rpmsg
收到数据后将回调该函数。
从data
获得M4发来的数据,通过rpmsg_send()
将数据发给M4。
将该驱动编译成模块,并在A7中加载。
3.2.2 M4准备
参考前面的示例,依次在STM32CubeMX配置UART5、IPCC和OPENAMP,然后生成初始化代码。
- 1.建一个rpmsg通道1OPENAMP_create_endpoint(&resmgr_ept, RPMSG_SERVICE_NAME, RPMSG_ADDR_ANY, rx_callback, NULL);
注意这里的RPMSG_SERVICE_NAME
和驱动里rpmsg_device_id
结构体的.name
的名字一样。
之后,一旦rpmsg
通道数据,将回调rx_callback()
函数。
2.编写接收回调函数
此函数里,需要将接收的数据复制到用户内存,并修改接收标志位。1234567static int rx_callback(struct rpmsg_endpoint *rp_chnl, void *data, size_t len, uint32_t src, void *priv){/* copy received msg, and raise a flag */memcpy(received_rpmsg, data, len > sizeof(received_rpmsg) ? sizeof(received_rpmsg) : len);printf("received_rpmsg=%s\r\n", received_rpmsg);rx_status = SET;return 0;3.主函数轮询RPMsg消息
OPENAMP_check_for_message()
查询MailBox状态。
当收到数据时,rx_callback()
会保存好收到数据,然后修改rx_status
标志位。
主函数里发现rx_status
标志位发生变化时,即可获取接收的数据。12345678910111213while (1){OPENAMP_check_for_message();/* USER CODE END WHILE */if (rx_status == SET){/* Message received: send back a message anwser */rx_status = RESET;/*rpmsg收到数据*/}/* USER CODE BEGIN 3 */}4.向M4发送数据
使用OPENAMP_send()
向A7发送数据。123456789101112if (++count < 100)sprintf((char *)msg, "hello world! M4->A7 %02ld", count);elsestrcpy((char *)msg, "goodbye!");if (OPENAMP_send(&resmgr_ept, msg, strlen((char *)msg) + 1) < 0){printf("Failed to send message\r\n");Error_Handler();}elseprintf("send_rpmsg=%s\r\n", msg);
3.2.3 M4完整代码
|
|
3.2.4 测试效果
首先加载驱动rpmsg_client_sample.ko
,并修改打印等级。
然后加载M4固件,可以看到A7和M4都同时发送和接收到相互之间的数据。
