引言
在嵌入式开发中,核间通讯(Inter-Processor Communication, IPC)是多核处理器系统实现高效协作的关键技术。STM32MP15x是ST于2019年发布的一款MPU产品,是ST推出的首款基于Arm® Cortex®-A7处理器的微处理器系列(MPU),旨在结合Cortex-A7和Cortex-M4处理器的优势,为嵌入式应用提供更高的性能和灵活性。
作为一款集成双核Cortex-A7和单核Cortex-M4的高性能微处理器,STM32MP157非常适合高实时性的应用场景,例如工业控制,数据采集。开发者无需在Linux内核驱动开发以及实时性调优上花费更多精力,只需关注在M4核心上实现高实时性任务并与A7核进行数据共享,本文将详细介绍RPMsg-TTY/SDB两种方式的核间通讯使用方法。
如果你是第一次接触STM32MP15的核间通讯,建议先参考正点原子,野火或其他培训机构的配套教程,他们大多是从理论出发,介绍核间通讯原理,你会了解如何在A7中启动M4的固件,并使用TTY虚拟串口方式,使用简单的echo测试命令测试核间数据通讯,但是并没有详细介绍编程方式的实现,更没有介绍适用于高数据吞吐量SDB方式的使用方法。
参考资料
本文将从ST的基于STM32MP15x-DK的逻辑分析仪示例应用展开,与大家分享核间通信的2种通讯方式,分别适用于低数据吞吐量的RPMsg-TTY方式和高数据吞吐量的RPMsg-SDB方式。本文参考资料详见👉如何与协处理器交换数据缓冲区👈,推荐大家先阅读。
调试思路
首先明确思路,既然是核间通讯,那必然有2套应用程序,你可以理解为:一个是运行在M4核上的单片机固件程序,一个是A7核应用程序。
通过ST对逻辑分析仪的示例介绍得知,我们需要分析该示例的2套代码
该示例的主要功能为:用户于A7侧应用程序的用户界面按下Start按钮,启动采样,由M4侧使用定时器实现对GPIO电平以特定采样率进行采样后,将数据交给A7侧处理。通过一下两张示意图可以对两种数据共享方式有清晰的了解。
对于低采样率:M4侧使用定时器控制采集并使用DMA,在触发DMA中断时将数据暂存至M4侧的SRAM,随后将数据通过RPMsg_tty驱动实现将数据转发至A7侧,用户侧通过虚拟串口取到数据。
对于高采样率:M4侧使用定时器控制采集并使用DMA,在触发DMA中断时将数暂存至M4侧的SRAM,随后数据通过RPMsg-sdb驱动实现将数据直接写入DDR,采集相关命令与状态同步仍然使用RPMsg-tty方式实现。通过共享内存的方式实现高速率的数据写入与读取。
我们需要在这2份源码中寻找线索:
- 适用于低数据吞吐量的核间通讯方式:RPMsg-tty
- 适用于高数据吞吐量的核间通讯方式:RPMsg-sdb
分析M4固件源码
M4固件的功能
在上面提到的WiKi文档中ST介绍了在逻辑分析仪示例中M4固件的主要功能:
- 通过RPMsg-tty通道接收来自Linux应用程序的命令,命令中包含了DDR缓冲区的数量。
- 从RPMsg-sdb驱动程序中接受包含待操作的DDR缓冲区物理地址和大小信息,该操作去要在M4固件的初始化阶段完成,即使sdb模式只在高采样率时使用。
- 从RPMsg-tty通道接收采集开始/停止指令。
- 使用DMA通过设定的采样率对GPIO电平进行采样,写入SRAM。
- 低采样率时使用TTY(虚拟串口)方式,将数据发送至A7侧应用程序。
- 高采样率时使用SDB方式将数据写入DDR,由A7侧程序应用读取。
核心代码
时钟初始化
MP157拥有不同的启动方式,当仅调试M4固件而不启动A7系统时一般使用工程模式启动,M4程序程序在功能完善后或需要与A7进行联动时,由A7系统内控制M4固件启动。这两种不同启动方式对于M4固件而言在时钟初始化时略有不同。
可参考示例代码main.c
第875行,系统时钟配置和核间通讯底层相关初始化函数在调用时需要注意
if (IS_ENGINEERING_BOOT_MODE())
{
/* Configure the system clock */
SystemClock_Config();
}
if (IS_ENGINEERING_BOOT_MODE())
{
/* Configure the peripherals common clocks */
PeriphCommonClock_Config();
}
else
{
/* IPCC initialisation */
MX_IPCC_Init();
/* OpenAmp initialisation ---------------------------------*/
MX_OPENAMP_Init(RPMSG_REMOTE, NULL);
}
初始化RPMsg功能
M4侧固件程序初始化了3个RPMsg设备,包含2路虚拟串口和一个SDB设备,对应初始化代码在 main
函数中很容易找到。
/*
* Create HDR device
* defined by a rpmsg channel attached to the remote device
*/
hsdb0.rvdev = &rvdev;
log_info("SDB OpenAMP-rpmsg channel creation\n");
if (RPMSG_HDR_Init(&hsdb0) != RPMSG_HDR_OK)
{
log_err("RPMSG_HDR_Init HDR failed.\n");
Error_Handler();
}
/*
* Create Virtual UART devices
*/
log_info("Virtual UART0 OpenAMP-rpmsg channel creation\r\n");
if (VIRT_UART_Init(&huart0) != VIRT_UART_OK)
{
log_err("VIRT_UART_Init UART0 failed.\r\n");
Error_Handler();
}
log_info("Virtual UART1 OpenAMP-rpmsg channel creation\r\n");
if (VIRT_UART_Init(&huart1) != VIRT_UART_OK)
{
log_err("VIRT_UART_Init UART1 failed.\r\n");
Error_Handler();
}
RPMSG_HDR_Init
函数用于初始化SDB模相关功能,VIRT_UART_Init
函数用于初始化TTY模式相关功能。对应3个操作句柄角色如下:
- huart0:低采样率下数据传输通道
- huart1:异常消息字符串传输通道
- hsdb0:高采样率下数据传输通道
设置数据接收回调函数
RPMsg在任何模式下M4侧固件均可接收数据,同样需要为它制定数据接收时的回调函数,可参看main
函数第939行。
/*Need to register callback for message reception by channels*/
if (RPMSG_HDR_RegisterCallback(&hsdb0, RPMSG_HDR_RXCPLT_CB_ID, SDB0_RxCpltCallback) != RPMSG_HDR_OK)
{
Error_Handler();
}
/*Need to register callback for message reception by channels (not for UART1 as used only for trace outpout) */
if (VIRT_UART_RegisterCallback(&huart0, VIRT_UART_RXCPLT_CB_ID, VIRT_UART0_RxCpltCallback) != VIRT_UART_OK)
{
Error_Handler();
}
RPMSG_HDR_RegisterCallback
函数负责注册RPMSG_HDR_RegisterCallback
函数,当A7侧程序初始化SDB模式时,会得到一块可用的DDR共享内存,此时会调用SDB0_RxCpltCallback
函数。M4侧程序在其中可以获得相对于自身可操作的一块内存地址和大小,这对后续M4固件操作DDR内存起着关键作用。
VIRT_UART_RegisterCallback
函数负责注册VIRT_UART0_RxCpltCallback
函数,当RPMsg-TTY虚拟串口huart0
接收到来自A7侧程序发送来的数据时,将会调用VIRT_UART0_RxCpltCallback
函数,可在该函数中接收来自huart0
的数据。
发送数据
前面提到过,huart0
用于在低采样率下向A7侧传输数据,hsdb0
用于在高采样率下向DDR写数据。
huart0
发送数据相关函数可参考LAStateMachine
状态机函数第661行和702行,函数原型如下
VIRT_UART_StatusTypeDef VIRT_UART_Transmit(VIRT_UART_HandleTypeDef *huart, const void *pData, uint16_t Size);
hsdb0
向DDR写数据相关函数可参考LAStateMachine
状态机函数第753行和810行,函数原型如下
RPMSG_HDR_StatusTypeDef RPMSG_HDR_Transmit(RPMSG_HDR_HandleTypeDef *hdr, uint8_t *pData, uint16_t Size);
总结
M4侧的示例代码框架使用状态机实现,建议关注前面介绍的几个核心函数,包括DMA半传输中断、DMA传输中断回调函数,理解程序中各标志位功能及作用。
用户在使用RPMsg-SDB模式时,务必要确保使用RPMSG_HDR_Init
函数完成SDB初始化、使用RPMSG_HDR_RegisterCallback
函数完成SDB回调函数的注册,确保从中获取到可操作的内存指针和大小后,才能调用RPMSG_HDR_Transmit
函数写数据。
分析A7程序源码
A7侧应用程序负责实现用户交互功能以及数据的接收及展示,共2个main函数供我们参考。我们只关心RPMsg部分功能,因此需要研究backend.c
中main函数的代码。打开源码路径recipes-graphics/st-software/logic-analyser-backend/backend.c
。
获取M4侧状态并加载固件
M4侧固件需要在A7侧程序运行前加载并启动,A7侧程序运行时先判断M4状态
backend.c
中的以copro_
开头的函数是A7侧程序对协处理器也就是M4处理器的常用操作,可以参考并复用。
A7侧应用程序在判断M4侧加载并运行指定固件后,创建了2个线程分别用来处理TTY虚拟串口方式和SDB共享内存方式的任务。
TTY虚拟串口线程
该线程专门用来处理RPMsg-TTY模式下数据的接收与发送,在A7侧设备路径为/dev/ttyRPMSGx
,用户只需调用copro_openTtyRpmsg
、copro_readTtyRpmsg
、copro_writeTtyRpmsg
、copro_closeTtyRpmsg
函数即可实现虚拟串口方式下的数据收发及控制。
用户可以直接复用这些函数到自己的工程,但需要注意自己实际使用的设备路径及文件描述符。
SDB共享内存线程
该线程专门用来处理RPMsg-SDB模式下数据的读取,所谓SDB,就是共享内存(Shared Data Buffer)。A7侧使用RPMsg-SDB方式获取数据的方式非常简单,当M4侧固件加载并运行时,因为调用了RPMSG_HDR_Init
函数,内部使用OPENAMP_create_endpoint
函数创建了"rpmsg-sdb-channel"服务,A7侧的文件系统设备路径会出现/dev/rpmsg_sdb
设备,它提供了一个分配和管理共享缓冲区的接口。
我们只需要打开该设备,并使用mmap
函数申请内存并为缓冲区注册事件,A4侧就会在对应的回调函数中得到M4自身可访问的内存地址和大小。
当M4侧将数据写满对应内存时,A7侧会触发事件,用于通知缓冲区已满。
打开SDB设备
打开RPMsg-SDB设备,获取文件描述符,可参考backend.c
第809行,得到SDB设备描述符mFdSdbRpmsg
。
int ret, rc, i, n;
int buffIdx = 0;
char buf[16];
char dbgmsg[80];
rpmsg_sdb_ioctl_get_data_size q_get_data_size;
char *filename = "/dev/rpmsg-sdb";
rpmsg_sdb_ioctl_set_efd q_set_efd;
mFdSdbRpmsg = open(filename, O_RDWR);
assert(mFdSdbRpmsg != -1);
申请内存与事件注册
A7侧需要申请内存让M4侧使用,同时需要注册事件,当M4将缓冲区填满时告诉A7侧数据已满,可参考backend.c
第819行。
for (i=0;i<NB_BUF;i++){
// Create the evenfd, and sent it to kernel driver, for notification of buffer full
efd[i] = eventfd(0, 0);
if (efd[i] == -1)
error(EXIT_FAILURE, errno,
"failed to get eventfd");
printf("\nCA7 : Forward efd info for buf%d with mFdSdbRpmsg:%d and efd:%d\n",i,mFdSdbRpmsg,efd[i]);
q_set_efd.bufferId = i;
q_set_efd.eventfd = efd[i];
if(ioctl(mFdSdbRpmsg, RPMSG_SDB_IOCTL_SET_EFD, &q_set_efd) < 0)
error(EXIT_FAILURE, errno,
"failed to set efd");
// watch eventfd for input
fds[i].fd = efd[i];
fds[i].events = POLLIN;
mmappedData[i] = mmap(NULL,
DATA_BUF_POOL_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE,
mFdSdbRpmsg,
0);
printf("\nCA7 : DBG mmappedData[%d]:%p\n", i, mmappedData[i]);
assert(mmappedData[i] != MAP_FAILED);
fMappedData = 1;
sleep_ms(50);
}
宏定义NB_BUF
的值为10,共循环10次,每次循环使用eventfd
函数创建事件得到事件的文件描述符,随后通过调用ioctl
函数将事件文件描述符和缓冲区ID传递给mFdSdbRpmsg
所对应设备的驱动程序,驱动层会处理缓冲区ID和事件文件描述符。当缓冲区写满后会触发对应的事件描述符,应用层就知道是哪个缓冲区完成了数据写入操作。
mmappedData
数组用于存放每次调用mmap
函数后得到的申请到的内存地址,这与索引相匹配。
fds
数组中存放着所有事件描述符的值,同样与索引相匹配。
轮询检查事件
前面完成了事件的创建以及内存的申请,此时M4侧固件也到了A7侧在申请内存后在SDB0_RxCpltCallback
回调函数函数中得到的内存地址及大小。我们要做的就是在A7侧等待事件的触发,通过触发事件的文件描述符得到缓冲区的索引,找到内存,读取数据。
参考代码backend.c
第846行,代码过多,这里先简化更易于理解。
while (1) {
if (mMachineState == STATE_SAMPLING_HIGH) {
ret = poll(fds, NB_BUF, TIMEOUT * 1000);
if (ret == -1)
// 错误
perror("poll()");
else if (ret == 0){
// 超时
printf("CA7 : No buffer data within %d seconds.\n", TIMEOUT);
}
if (fds[mDdrBuffAwaited].revents & POLLIN) {
// 索引"mDdrBuffAwaited"的事件已触发
// ...
}
else {
// 索引"mDdrBuffAwaited"的事件已触发
// 索引++
mDdrBuffAwaited++;
// ...
}
}
sleep_ms(5); // give time to UI
}
首先在while循环中轮询判断在高采样率时,事件是否触发,随后调用poll
函数,对已经存放所有事件文件描述符的数组fds
进行监视。当fds
数组中至少一个文件描述符变得可读(即触发 POLLIN
事件)时,继续向下执行。
当poll
函数在超时时间内返回说明事件触发,以mDdrBuffAwaited
作为轮询索引找到具体的事件文件描述符。
接收数据
当获取到事件触发即fds[mDdrBuffAwaited].revents & POLLIN
返回值为true时,即可通过mmappedData[mDdrBuffAwaited]
得到待读取的内存地址。参考代码见backend.c
第896行。
rc = read(efd[mDdrBuffAwaited], buf, 16);
if (!rc) {
printf("CA7 : stdin closed\n");
return 0;
}
/* Get buffer data size*/
q_get_data_size.bufferId = mDdrBuffAwaited;
if(ioctl(mFdSdbRpmsg, RPMSG_SDB_IOCTL_GET_DATA_SIZE, &q_get_data_size) < 0) {
error(EXIT_FAILURE, errno, "Failed to get data size");
}
if (q_get_data_size.size) {
mNbUncompMB += 2;
mNbUncompData += q_get_data_size.size;
mNbUncompData += q_get_data_size.size; // need twice as we missed one
unsigned char* pData = (unsigned char*)mmappedData[mDdrBuffAwaited];
// save a copy of 1st data
mByteBuffCpy[0] = *pData;
gettimeofday(&tval_after, NULL);
timersub(&tval_after, &tval_before, &tval_result);
printf("[%ld.%06ld] sdb_thread data EVENT mDdrBuffAwaited=%d mNbUncompData=%u \n",
(long int)tval_result.tv_sec, (long int)tval_result.tv_usec, mDdrBuffAwaited,
mNbUncompData);
gdk_threads_add_idle (refreshUI_CB, window);
}
首先调用read
函数,从 eventfd
文件描述符 efd[mDdrBuffAwaited]
中读取最多 16 字节的数据到 buf
中。read()
返回值是读取的字节数。如果成功读取数据,rc
将是大于零的值,意味着该事件文件描述符对应的缓冲区有内容可读。
准备接收数据,设置 q_get_data_size
结构体中的 bufferId
字段为当前正在处理的缓冲区索引 mDdrBuffAwaited
。这个结构体将用于获取缓冲区的数据大小。
pData
实际上就是可以直接读取数据的地址了。
总结
ST以逻辑分析仪的示例代码清晰的展示了RPMsg TTY与SDB方式的使用方法,对于吞吐量较大的数据,推荐大家使用SDB方式。同时在M4侧通过SDB向DDR写入数据时可以使用TTY方式向A7侧传递包含进度的消息,在A7侧通过进度判断写入是否超时。