你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【STM32MP157】从ST官方例程中分析RPMsg-TTY/SDB核间通信的使用方法

[复制链接]
DM9600 发布时间:2024-12-6 19:05

引言

在嵌入式开发中,核间通讯(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侧,用户侧通过虚拟串口取到数据。

How2bigdatarchiTTY1.jpg

对于高采样率:M4侧使用定时器控制采集并使用DMA,在触发DMA中断时将数暂存至M4侧的SRAM,随后数据通过RPMsg-sdb驱动实现将数据直接写入DDR,采集相关命令与状态同步仍然使用RPMsg-tty方式实现。通过共享内存的方式实现高速率的数据写入与读取。

How2bigdatarchiDDR1.jpg

我们需要在这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状态

  • 使用copro_isFwRunning函数检查M4处理器是否已运行固件,

    • 如果已运行M4侧固件则使用copro_getFwName函数获取当前运行的固件名称
      • 如果当前运行的是指定的M4固件则跳转到fwrunning部分执行后面的代码。
      • 如果当前运行的不是指定的M4固件则调用copro_stopFw函数停止运行M4侧固件。
  • 如果M4侧固件未运行,则加载指定固件并运行。

backend.c中的以copro_开头的函数是A7侧程序对协处理器也就是M4处理器的常用操作,可以参考并复用。

A7侧应用程序在判断M4侧加载并运行指定固件后,创建了2个线程分别用来处理TTY虚拟串口方式和SDB共享内存方式的任务。

TTY虚拟串口线程

该线程专门用来处理RPMsg-TTY模式下数据的接收与发送,在A7侧设备路径为/dev/ttyRPMSGx,用户只需调用copro_openTtyRpmsgcopro_readTtyRpmsgcopro_writeTtyRpmsgcopro_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侧通过进度判断写入是否超时。

How2bigdatarchiTTY1.jpg
How2bigdatarchiDDR1.jpg
收藏 评论0 发布时间:2024-12-6 19:05

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32Cube扩展软件包
意法半导体边缘AI套件
ST - 理想汽车豪华SUV案例
ST意法半导体智能家居案例
STM32 ARM Cortex 32位微控制器
关注我们
st-img 微信公众号
st-img 手机版