ESM7000异构CPU实时应用之二
基于rpmsg的通讯机制

 2021-5-13 10:24:42     作者:黄志超    

  英创公司推出了基于ESM7000的异构CPU架构的实时应用,其中RPMsg提供了Crotex-A7核心与M4核心之间的双向数据通道。RPMsg(Remote Processor Messaging)是基于virtio的消息传递总线,它允许在非对称多处理器(AMP)中存在的同构或异构内核上运行的独立软件上下文之间进行处理器间通信。目前RPMsg已经成为非对称多处理系统中核与核之间的标准通信方式,广泛的被应用在各种场合中。


  为了更加方便客户使用,英创公司提供了驱动emx_rpmsg_tty.ko,这个驱动会将RPMsg虚拟为一个串口设备/dev/ttyEMX,这时客户只需要使用标准的串口程序操作/dev/ttyEMX就可以在User app中和M4进行通讯了。操作方法和标准串口完全一致,调用串口的发送函数是向M4发送数据,调用读取函数则是读取M4发送过来的数据。客户还可以直接使用我们在光盘资料中提供的串口的例程step2_serialtest来测试,将OpenPort函数中打开的设备节点由/dev/ttySx改为/dev/ttyEMX就可以了,十分容易。


  RPMsg适合小数据的传输,可以用于Crotex-A7核心和M4之间的事件传输。大数据的传输如果采用RPMsg,效率会比较低,而且会占用很多系统资源,所以英创公司提供了采用共享内存的方式来传输大数据的方法。


  在此基础上英创公司定义了一套较简单灵活的通讯协议作为例子提供给客户参考。旨在为客户的开发抛砖引玉,缩短客户的开发周期,下面来详细介绍英创公司定义的这套通讯协议。通讯协议主要由两部分构成,一部分是事件传输,另外一部分是大数据的传输。


  事件传输主要负责传输操作指令和设置指令等,都是由linux系统的User app发起,M4收到后会执行相应的操作,然后回复(ACK),根据回复User app就能够知道通信是否成功。而大数据的传输由M4发起,M4将特定的消息通过RPMsg传输给User app,User app收到后去读取共享内存数据,可以参考下面的框图:


ESM7000异构CPU实时应用之二 基于rpmsg的通讯机制.png


  首先是定义通讯使用的数据结构体,User app和M4之间的通讯都是以这个结构体为单位,具体定义如下:


struct emx_rpmsg_t  
{  
    //时间戳,用于保存系统内部定时器的值  
    uint32_t    time_stamp;  
    //操作指令  
    uint32_t    cmd;  
    //标志,用于一些操作指令的附加功能  
    uint32_t    flags;  
    //data中数据的长度  
    uint32_t    len;  
    //预留,用于保存数据  
    uint8_t     data[0];  
}__attribute__ ((packed));

 

  结构体中cmd变量用于保存需要执行的操作指令,对操作指令具体定义如下:


//操作指令掩码  
#define CMD_MARK                (0x55AA << 16)  
//复位指令,由Crotex-A7发送到M4上,M4收到后执行相应的操作  
#define CMD_CODE_RESET          (CMD_MARK | 0X01)  
//设置指令,由Crotex-A7发送到M4上,设置内容可由预留的data变量保存,M4收到后执行相应的操作  
#define CMD_CODE_SETUP          (CMD_MARK | 0X02)  
//启动指令,由Crotex-A7发送到M4上,M4收到后执行相应的操作  
#define CMD_CODE_RUN            (CMD_MARK | 0X03)  
//停止指令,由Crotex-A7发送到M4上,M4收到后执行相应的操作  
#define CMD_CODE_STOP           (CMD_MARK | 0X04)  
//状态查询指令,由Crotex-A7发送到M4上,M4收到后回复相应状态    
#define CMD_CODE_STATUS         (CMD_MARK | 0X20)  
//数据指令,由M4发送到Crotex-A7上,表示已经在共享内存填写了数据  
#define CMD_CODE_DATA           (CMD_MARK | 0X40)  
//指令执行成功标志    
#define CMD_CODE_ACK_SUCCESS     0x8000  
//指令执行失败标志    
#define CMD_CODE_ACK_FAILED      0x4000

 

  第一步先介绍事件传输的情况,事件传输主要负责操User app和M4之间的消息传递,比如操作指令或者设置指令等。将需要执行的指令赋值给emx_rpmsg_t结构体的成员变量cmd,然后将结构体发送给M4就完成了一次通讯。当M4收到指令后,会执行对应的操作并回复相应的结果,这时通过User app就可以接收到M4发送过来的emx_rpmsg_t数据结构体,回复的数据会在收到的成员变量cmd的基础上,或上成功或者失败标志。所以User app通过判断回发数据结构体成员变量cmd的值,就可以知道指令执行的结果。英创公司提供了测试好的函数供客户使用,下面分别是User app中和M4 app中的代码:


  User app中的代码:


/* 
* Send_cmd函数实现向M4发送操作指令 
* 
* 参数说明: 
* cmd:定义的操作指令 
* param:参数,有一些指令可能需要参数的辅助,参数存放在data成员变量中 
* len:参数字节长度,保存在len成员变量中 
* 
*/  
int Rpmsg::Send_cmd( uint32_t cmd, void *param, int len )  
{  
    struct emx_rpmsg_t *msg_tx;  
    struct emx_rpmsg_t *msg_rx;  
    char   Buf[4095];  
    int  data_len, sendlen, recvlen;  
  
    data_len = sizeof(struct emx_rpmsg_t) + len;  
    msg_tx = (struct emx_rpmsg_t *)malloc(data_len);  
  
    //给msg_tx赋值  
    msg_tx->cmd = cmd;  
    msg_tx->len = len;  
    if(param != NULL)  
        memcpy(msg_tx->data, param, len);  
    //发送指令  
    sendlen = write(m_fd, msg_tx, data_len);  
    if(sendlen != data_len) {  
        printf("failed to send cmd!\n");  
        return -1;  
    }  
  
    //发送完成后等待返回数据  
    while(1) {  
        recvlen = read( m_fd, Buf, 4096 );  
        msg_rx = (struct emx_rpmsg_t *)malloc(recvlen);  
        memcpy(msg_rx, Buf, recvlen);  
        if(msg_rx->cmd == CMD_CODE_DATA) {  
            free(msg_rx);  
            continue;  
        } else  
        {  
            break;  
        }  
    }  
  
    //判断是否成功执行操作  
    if((msg_tx->cmd | CMD_CODE_ACK_SUCCESS) == msg_rx->cmd) {  
        printf("Send successfully\n");  
        free(msg_tx);  
        free(msg_rx);  
        return 0;  
    } else {  
        printf("Send failed\n");  
        free(msg_tx);  
        free(msg_rx);  
        return -1;  
    }  
}


  M4 app中的代码:


static void RpmsgRevTask(void *pvParameters)  
{  
    uint32_t samplingRate, cmd;  
    int result, len;  
    rpmsg_packed_t *rpmsg = (rpmsg_packed_t *)msg_buf;  
  
    RPMSG_Init();  
  
    for (;;)  
    {  
        /* Get RPMsg rx buffer with message */  
        result = RPMSG_Recv(msg_buf, &len, 0xFFFFFFFF);  
        assert(result == 0);  
    
        cmd = rpmsg->cmd;  
        CMD_ACK_SUCCESS(rpmsg->cmd);  
        switch (cmd)  
        {  
        case CMD_CODE_RESET:  
            sprintf((char *)rpmsg->data, "EMX2001 v%d.%c%c%c%c%c%c.%d[Ads8588s 16bit 8-Channel ADC], Maximum sampling rate:100KHz",  
                    VERSION_MAJOR, COMLETE_TIME);  
  
            rpmsg->len = strlen((char *)rpmsg->data);  
            len = sizeof(rpmsg_packed_t) + rpmsg->len;  
            break;  
  
        case CMD_CODE_SETUP:  
            if (rpmsg->len == 4)  
            {  
                samplingRate = *((uint32_t *)rpmsg->data);  
                PRINTF("ADC Sampling rate = %d.\r\n", samplingRate);  
                if (samplingRate > 100000)  
                {  
                    rpmsg->flags = 1;  
                }  
                else  
                {  
                    adc_stop();  
                    pwmConfig.period_ns = 1000000000 / samplingRate;  
                }  
            }  
            else  
            {  
                CMD_ACK_FAILED(rpmsg->cmd);  
            }  
            break;  
  
        case CMD_CODE_START:  
            adc_start();  
            break;  
  
        case CMD_CODE_STOP:  
            adc_stop();  
            break;  
  
        case CMD_CODE_STATUS:  
            break;  
  
        default:  
            CMD_ACK_FAILED(rpmsg->cmd);  
        }  
          
        result = RPMSG_Send(rpmsg, len);  
        assert(result == 0);  
    }  
}


  可以看到这个函数会将填写的操作指令和参数发送给M4,并且会等待M4的回发响应数据,然后判断是否成功执行。下面给出两个简单的实例供客户参考,第一个是复位指令:


ret = Send_cmd(CMD_CODE_RESET, NULL, 0);  
if( ret<0 )  
{  
    printf( "send failed\n");  
}

 

  第二个是设置指令,示例代码是设置ADC采样频率为10K:


int freq = 10000;    
           
ret = Send_cmd(CMD_CODE_SETUP, (void *)&freq, sizeof(freq));    
if( ret<0 )    
{    
    printf( "send failed\n");    
}


  接下来介绍大数据的传输方式,大数据的传输是由M4主动发起。当User app接收到的成员变量cmd的值为CMD_CODE_DATA时,就表示M4已经将数据填入了共享内存中了,这时就需要User app去读取。因为数据是采用共享内存的方式,User app中提前映射好共享内存,当需要读取数据时,直接调用memcpy函数即可。


  内存映射的代码如下:


int         mem_fd;  
void        *base;  
  
mem_fd = open("/dev/mem", O_RDWR|O_SYNC);  
printf("mem_fd is %d\n", mem_fd);  
  
/* mmap mem */  
base = mmap(  
    NULL,             //Any adddress in our space will do  
    MEM_DEV_SIZE,    //Map length  
    PROT_READ|PROT_WRITE,// Enable reading & writting to mapped memory  
    MAP_SHARED,       //Shared with other processes  
    mem_fd,               //File to map  
    MEM_BASE          //Offset to DMTimer peripheral  
);  
  
close(em_adc.mem_fd);

 

  下面是分别是M4 app写共享内存和Linux系统User app读取共享内存数据的代码,示例中有两块映射区域,通过emx_rpmsg_t结构体成员变量flags来区分,User app的这部分代码在接收线程中运行,一旦收到数据就会进行读取:


  User app部分:


int Rpmsg::PackagePro( char* Buf, int len )  
{  
    int                 i1, num;  
    int         buf_len;  
    struct emx_rpmsg_t  msg;  
      
    memcpy(&msg, (char *)Buf, sizeof(struct emx_rpmsg_t));  
    if(msg.cmd == CMD_CODE_DATA) {  
        Mlen = msg.len;  
        //读取内存数据  
        if(msg.flags == 0) {  
            memcpy(MemBuf, (void *)base0, msg.len);  
        }  
        else {  
            memcpy(MemBuf, (void *)base1, msg.len);           
        }  
        //处理数据,这里只是简单的保存  
        save_data(MemBuf, Mlen);  
    }  
    return 0;  
}


  M4 app部分:


/*! 
* SPI中断服务程序,为了提高SPI操作效率,当SPI FIFO中接收超过32个DWORD数时产生中断 
* 在中断服务程序中将SPI数据读出 
*/  
void ESM_SPI1_HANDLER(void)  
{  
    uint32_t tmp;  
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;  
  
    /* Disable all SPI interrupt */  
    ECSPI_INTREG_REG(ESM_SPI1) = 0;  
  
    while (ECSPI_STATREG_REG(ESM_SPI1) & ECSPI_STATREG_RR_MASK)  
    {  
        tmp = ECSPI_RXDATA_REG(ESM_SPI1);  
        /* 将AD数据存放在M4与A7(Linux)的共享内存中 */  
        *((uint16_t *)mem_addr + shared_buf_idx++) = (uint16_t)(tmp >> 16);  
        *((uint16_t *)mem_addr + shared_buf_idx++) = (uint16_t)tmp;  
  
        if (shared_buf_idx >= sampling_len)  
        {  
            /* 当采样到指定长度的AD数据后,对乒乓buffer进行切换,并将xDataReadySemaphore信号量设置为有效 */  
            shared_buf_idx = 0;  
            if (mem_addr == SHARED_MEMORY_BASE + SHARED_MEMORY_BLOCK0)  
            {  
                data_packed.flags = 0;  
                mem_addr = SHARED_MEMORY_BASE + SHARED_MEMORY_BLOCK1;  
            }  
            else  
            {  
                data_packed.flags = 1;  
                mem_addr = SHARED_MEMORY_BASE + SHARED_MEMORY_BLOCK0;  
            }  
  
            /* Unlock the task to process the event. */  
            xSemaphoreGiveFromISR(xDataReadySemaphore, &xHigherPriorityTaskWoken);  
  
            /* Perform a context switch to wake the higher priority task. */  
            portYIELD_FROM_ISR(xHigherPriorityTaskWoken);  
        }  
    }  
  
    ECSPI_ClearStatusFlag(ESM_SPI1, ecspiFlagRxfifoDataRequest);  
  
    ECSPI_SetIntCmd(ESM_SPI1, ecspiFlagRxfifoDataRequest, true);  
}


  在英创公司给出的协议基础上,客户可以进行扩展并实现需要的应用。英创公司在后续也会给出更多实例的例子供客户参考。如果对于这一套异构CPU平台十分熟悉,也可以根据需求自行设计方案。


  如果需要详细的例程代码,可与英创的工程师联系。