1. 前言
既然是多核编程,核间的数据交互是避免不了的,因此常常会使用到IPC通信技术。掌握TMS320F28377D芯片的IPC通信技术是在2021年年底,距今已半年了。只怪当初没有好好做个记录,现在想单独捡起来写成blog还需要好好回忆回忆。首先给一个参考代码路径:
C:\ti\controlSUITE\device_support\F2837xD\v210\F2837xD_examples_Dual\cpu02_to_cpu01_ipcdrivers
在之前讲到的28335和28377D的编程入门都有提到要安装ti提供的controlSUITE软件,这个软件不仅提供芯片的源文件、库文件、cmd文件等,同样还提供一些example便于dsp的使用者借鉴。
下面我慢慢回忆,断断续续谈一谈我对IPC的理解
在IPC中,我认为最关键的是: 有一块cpu1和cpu2都可以访问的共享内存空间
而IPC做了什么事情呢。比如cpu1要给cpu2传输数据,cpu1就先把数据拷贝到共享内存空间,然后再设置一个标志, cpu2检测到这个标志就会触发中断,并且在中断中读取共享内存空间的数据。核间通信应该比什么串口啊 网口啊都要快,核间只传送标志,数据都在共享内存里面。
Tips: 这个共享内存空间,是有固定的地址段的,会操作cmd的大佬一定要注意,在修改cmd文件的时候,别占用这块共享内存空间,特别是程序大时,要扩充全局变量的地址空间,可千万别把共享内存的地址空间给扩充进去了。【血泪史】 在某篇我的周报中写道: IPC通信问题已解决,问题原因是CPU1的全局变量占了IPC通信使用的共享内存的地址,解决办法修改cmd的.ebss(管全局变量的)段使用的内存区。】
当时觉得要做IPC核间通信,觉得这个怕是有点难,但掌握了以后,觉得也还好。
下面我将针对我实际开发的dsp项目的代码进行一个ipc代码剖析。把所有的零撒代码组合在一起,就可以形成完整ipc通信代码了。本例中,主要以cpu2向cpu1发起ipc通信请求。
2. CPU1代码剖析
2.1 ipcconfig
volatile tIpcController g_sIpcController1;
volatile tIpcController g_sIpcController2;
void ipcconfig(void){
EALLOW;
PieVectTable.IPC0_INT = &CPU02toCPU01IPC0IntHandler;
PieVectTable.IPC1_INT = &CPU02toCPU01IPC1IntHandler;
EDIS;
IPCInitialize(&g_sIpcController1, IPC_INT0, IPC_INT0);
IPCInitialize(&g_sIpcController2, IPC_INT1, IPC_INT1);
IER |= M_INT1;
PieCtrlRegs.PIEIER1.bit.INTx13 = 1; // CPU1 to CPU2 INT0
PieCtrlRegs.PIEIER1.bit.INTx14 = 1; // CPU1 to CPU2 INT0
}
提前统一说明:本文提到的函数,都是经过封装的,便于使用时模块化调用。
从ipcconfig函数的代码中,可看到 ipc使用到了两个中断处理函数 CPU02toCPU01IPC0IntHandler和CPU02toCPU01IPC1IntHandler。都是CPU02toCPU01,也就是CPU02可能在CPU01中触发两种类型的中断。
g_sIpcController1和g_sIpcController2则是在使用IPC相较于其他中断 额外需要定义的两个IPC中断会用到的结构体。结构体的定义是ti官方提供的,命名是完全参考ti官方提供的example程序。
2.2 ipcinit
void ipcinit(void){
pulMsgRam = (void *)CPU01TOCPU02_PASSMSG;
pulMsgRam[0] = (uint32_t)&cpu1tocpu2.data[0].s[0];
pulMsgRam[1] = (uint32_t)&cpu2tocpu1.data[0].s[0];
IpcRegs.IPCSET.bit.IPC17 = 1;
cpu1tocpu2.data[0].f[0] = 0.1234567;
cpu1tocpu2.data[63].f[1] = 3.14;
}
其中
#define CPU01TOCPU02_PASSMSG 0x0003FFF4
这个地址,是从example上借鉴过来的,应该是指定的。 此地址应该是 CPU1和CPU2都能访问的地址,在后续CPU2代码剖析中会谈到。
下面在看看ipcinit其他用到的相关变量定义
uint32_t *pulMsgRam;
struct ipcstruct cpu2tocpu1;
struct ipcstruct cpu1tocpu2;
union data_type{
Uint16 s[4];
int32 i[2];
float32 f[2];
float64 d;
};
struct ipcstruct{
union data_type data[64];
};
pulMsgRam是一个uint32_t类型的指针,作用是用来使用CPU01TOCPU02_PASSMSG这个地址。代码可以使用pulMsgRam来访问以CPU01TOCPU02_PASSMSG为起始地址的 数据空间。然后定义了有两个结构体变量cpu2tocpu1和cpu1tocpu2,该结构是作者本人声明的结构体类型。其作用是用于存储,cpu2 传到 cpu1这边来的数据、cpu1要发送到cpu2那边去的数据。
注意以下两句赋值语句
pulMsgRam[0] = (uint32_t)&cpu1tocpu2.data[0].s[0];
pulMsgRam[1] = (uint32_t)&cpu2tocpu1.data[0].s[0];
这两行程序的作用是把这两个结构体的首地址赋值到以CPU01TOCPU02_PASSMSG为起始地址的内存空间中去。 这个操作其特殊性应该是在于 你用于传输数据的结构体的地址,被赋值到指定的地址空间去了,而不是一个随便指定空间。这两句可太关键了。
关于下面这一句,其作用是标志位置位,其作用是当cpu2请求读取cpu1的一个数据块的内容的时候,会指定一个特定的标志位,当cpu1完成了cpu2的请求之后,这个标志位会被清零。而现在初始化时,先把它置位,其目的就是避免cpu2误判cpu1已经完成了cpu1的请求,导致程序出现错误。
IpcRegs.IPCSET.bit.IPC17 = 1;
2.3 CPU1的IPC中断处理函数
__interrupt void CPU02toCPU01IPC0IntHandler(void){
tIpcMessage sMessage;
while(IpcGet(&g_sIpcController1, &sMessage,
DISABLE_BLOCKING)!= STATUS_FAIL)
{
switch (sMessage.ulcommand)
{
case IPC_SET_BITS:
IPCRtoLSetBits(&sMessage);
break;
case IPC_CLEAR_BITS:
IPCRtoLClearBits(&sMessage);
break;
case IPC_DATA_WRITE:
IPCRtoLDataWrite(&sMessage);
break;
case IPC_DATA_READ:
IPCRtoLDataRead(&g_sIpcController1, &sMessage,
ENABLE_BLOCKING);
break;
case IPC_FUNC_CALL:
IPCRtoLFunctionCall(&sMessage);
break;
default:
break;
}
}
IpcRegs.IPCACK.bit.IPC0 = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}
__interrupt void CPU02toCPU01IPC1IntHandler (void) {
tIpcMessage sMessage;
while(IpcGet(&g_sIpcController2, &sMessage,DISABLE_BLOCKING)!= STATUS_FAIL){
switch (sMessage.ulcommand)
{
case IPC_SET_BITS_PROTECTED:
IPCRtoLSetBits_Protected(&sMessage); // Processes
// IPCReqMemAccess()
// function
break;
case IPC_CLEAR_BITS_PROTECTED:
IPCRtoLClearBits_Protected(&sMessage); // Processes
// IPCReqMemAccess()
// function
break;
case IPC_BLOCK_WRITE: // ▲▲▲ 重点关注
IPCRtoLBlockWrite(&sMessage);
fObsVar1 = cpu2tocpu1.data[0].f[0];
fObsVar2 = cpu2tocpu1.data[63].f[1];
fFtvx = cpu2tocpu1.data[4].f[0];
fFtvy = cpu2tocpu1.data[4].f[1];
if(cpu2tocpu1.data[3].s[0] == 0x01 ){
FSM_WorkMode = cpu2tocpu1.data[3].s[1];
}
fEncoderE = cpu2tocpu1.data[1].f[0];
fEncoderA = cpu2tocpu1.data[1].f[1];
fGyroE = cpu2tocpu1.data[2].f[0];
fGyroA = cpu2tocpu1.data[2].f[1];
fRtvE = cpu2tocpu1.data[3].f[0];
fRtvA = cpu2tocpu1.data[3].f[1];
break;
case IPC_BLOCK_READ: // ▲▲▲ 重点关注
IPCRtoLBlockRead(&sMessage);
if(cpu1tocpu2.data[0].s[0]==0x01){ cpu1tocpu2.data[0].s[0] = 0x00; }
break;
default:
break;
}
}
IpcRegs.IPCACK.bit.IPC1 = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}
两个中断处理函数各司其职, CPU02toCPU01IPC0IntHandler里面是主要是数据位操作、单个数据(最多32位)操作、函数远程调用这三个功能;而CPU02toCPU01IPC1IntHandler 最主要的,则是 一块(BLOCK)数据的操作。而作者主要用的就是这个一块数据的操作,前面的代码也提到了,我们用两个结构体,分别存储 cpu2 传到 cpu1的数据,还有就是cpu1要往cpu2送的数据。这两个结构体是肯定是Block操作。
注意代码中 // ▲▲▲ 重点关注的注释。
case IPC_BLOCK_WRITE: // ▲▲▲ 重点关注
IPCRtoLBlockWrite(&sMessage);
fObsVar1 = cpu2tocpu1.data[0].f[0];
fObsVar2 = cpu2tocpu1.data[63].f[1];
fFtvx = cpu2tocpu1.data[4].f[0];
fFtvy = cpu2tocpu1.data[4].f[1];
if(cpu2tocpu1.data[3].s[0] == 0x01 ){
FSM_WorkMode = cpu2tocpu1.data[3].s[1];
}
fEncoderE = cpu2tocpu1.data[1].f[0];
fEncoderA = cpu2tocpu1.data[1].f[1];
fGyroE = cpu2tocpu1.data[2].f[0];
fGyroA = cpu2tocpu1.data[2].f[1];
fRtvE = cpu2tocpu1.data[3].f[0];
fRtvA = cpu2tocpu1.data[3].f[1];
break;
case IPC_BLOCK_READ: // ▲▲▲ 重点关注
IPCRtoLBlockRead(&sMessage);
if(cpu1tocpu2.data[0].s[0]==0x01){ cpu1tocpu2.data[0].s[0] = 0x00; }
break;
当cpu1判断cpu2的请求是 往cpu1 IPC_BLOCK_WRITE的时候,它调用了IPCRtoLBlockWrite(&sMessage);这个函数,这个函数已经把cpu2放在共享内存的数据拷贝到cpu1指定的当中去了(cpu2也可以访问地址CPU01TOCPU02_PASSMSG的存储内容,cpu1在ipcinit的时候已经在里面指定了cpu1中两个结构体cpu1tocpu2、cpu2tocpu1的地址)。所以在调用IPCRtoLBlockWrite(&sMessage);函数之后,你可以看到下面的代码在直接使用cpu2tocpu1这个结构体。 这个时候cpu2的数据已经发过来了。
同理,当cpu1判断cpu2的请求是 从cpu1 IPC_BLOCK_READ的时候,它调用了IPCRtoLBlockRead(&sMessage);这个函数,这个函数的功能是把cpu2指定要读取地址中的数据拷贝共享内存当中去(cpu2也可以访问地址CPU01TOCPU02_PASSMSG的存储内容,cpu1在ipcinit的时候已经在里面指定了cpu1中两个结构体cpu1tocpu2、cpu2tocpu1的地址),它指定的地址,说白了就是cpu1tocpu2的地址。在调用了IPCRtoLBlockRead(&sMessage);这个函数之后,cpu1已经把cpu1tocpu2这个结构体的数据拷贝到了共享内存当中,并且把IPC17这个位清零,告诉cpu2,你的请求我已经帮你完成了。cpu2根据IPC17是否被清零,判断是否进入后续的处理/等待cpu1响应结束。
3. CPU2代码剖析
3.1 ipcconfig
void ipcconfig(void){
EALLOW;
PieVectTable.IPC0_INT = &CPU01toCPU02IPC0IntHandler;
PieVectTable.IPC1_INT = &CPU01toCPU02IPC1IntHandler;
EDIS;
IPCInitialize(&g_sIpcController1, IPC_INT0, IPC_INT0);
IPCInitialize(&g_sIpcController2, IPC_INT1, IPC_INT1);
IER |= M_INT1;
PieCtrlRegs.PIEIER1.bit.INTx13 = 1; // CPU2 to CPU1 INT0
PieCtrlRegs.PIEIER1.bit.INTx14 = 1; // CPU2 to CPU1 INT1
while(IpcRegs.IPCSTS.bit.IPC17 != 1){};
IpcRegs.IPCACK.bit.IPC17 = 1;
}
可以看到除了 最后两行代码,其他的都是和cpu1一样的。 cpu1配置的中断处理函数是说响应cpu2发起的操作的中断。而cpu2对于中断的配置,说白了是为了响应cpu1对cpu2发起的操作,而在我的程序中,我都是从cpu2向cpu1发起操作,所以等会看这两个中断处理函数,会很简单。因为正常情况下,是不会被触发的。
while(IpcRegs.IPCSTS.bit.IPC17 != 1){};
IpcRegs.IPCACK.bit.IPC17 = 1;
这个IPC17在第二章CPU1代码剖析中也讲了,主要的作用是用于给cpu2读取cpu1的数据的时候使用的,用于cpu1通知cpu2,我完成了你的操作。
3.2 ipcinit
void ipcinit(void){
pulMsgRam = (void *)CPU01TOCPU02_PASSMSG;
pusCPU01BufferPt = (void *)GS0SARAM_START;
pusCPU02BufferPt = (void *)(GS0SARAM_START + 256);
}
与cpu1的不同之处是 cpu2又引入了两个地址GS0SARAM_START 和 GS0SARAM_START + 256而GS0SARAM_START , 应该就是共享内存的起始地址了。
#define GS0SARAM_START 0xC000
3.3 ipcsend
void ipcsend(void){
// CPU2 obtains access to shared memory
IPCReqMemAccess(&g_sIpcController2, GS0_ACCESS, IPC_GSX_CPU2_MASTER, ENABLE_BLOCKING);
// Waiting for access
while(MemCfgRegs.GSxMSEL.bit.MSEL_GS0 != 1U){};
// Copy the data sent by CPU2 to CPU1 to the shared memory
memcpy((void *)pusCPU02BufferPt,(void *)&cpu2tocpu1.data[0].s[0],sizeof(struct ipcstruct));
// Inform CPU1 that I have sent data
IPCLtoRBlockWrite(&g_sIpcController2, pulMsgRam[1],(uint32_t)pusCPU02BufferPt,256, IPC_LENGTH_16_BITS,ENABLE_BLOCKING);
}
只有4行程序,下面进行逐行解析
IPCReqMemAccess(&g_sIpcController2, GS0_ACCESS, IPC_GSX_CPU2_MASTER, ENABLE_BLOCKING);
第一行的作用是类似于发送一个申请,去获取共享内存的使用权限(数据块操作的权限)。
while(MemCfgRegs.GSxMSEL.bit.MSEL_GS0 != 1U){};
第二行的作用是等待获得共享内存的使用权限,直到可以访问才结束这个while
memcpy((void *)pusCPU02BufferPt,(void *)&cpu2tocpu1.data[0].s[0],sizeof(struct ipcstruct));
第三行的作用是把cpu2要发送给cpu1的结构体数据, 以数据块的方式,拷贝到共享内存中去。
注意: 这里使用的目标地址是 pusCPU02BufferPt, 而pusCPU02BufferPt是(void *)(GS0SARAM_START + 256);也就是说,我们cpu2的用的共享内存地址是 共享内存起始地址 + 256的偏移地址。
显然, 那cpu1用的共享内存地址,就是 共享内存的起始地址。 而cpu1和cpu2的结构体的大小 就是256个dsp数据单元(16位)。
IPCLtoRBlockWrite(&g_sIpcController2, pulMsgRam[1],(uint32_t)pusCPU02BufferPt,256, IPC_LENGTH_16_BITS,ENABLE_BLOCKING);
第四行的这个函数,是ti官方提供的,可以在example程序里面找到,其作用就是 通知cpu1, 我已经把东西放在共享内存的某个地址( pusCPU02BufferPt)里面了, 我是发的是一个数据块,这个数据块共有256个数据单元(16位)。你把读的时候把我发的数据放在(pulMsgRam[1])这个地址里面吧。
当我们运行这个函数的时候, CPU1的程序会进入中断处理函数 CPU02toCPU01IPC1IntHandler中。然后把共享内存的数据, 拷贝的 “ pulMsgRam[1] ”这个空间所指定的地址中去 pulMsgRam[1]是什么呢?在cpu1的ipcconfig程序中已经赋值了,是 pulMsgRam[1] = (uint32_t)&cpu2tocpu1.data[0].s[0];也就是说, cpu2的这个结构体(cpu2tocpu1),就拷贝到了cpu1的同名结构体(cpu2tocpu1)中了。
注意到一个小问题, 在cpu2中是没有给pulMsgRam[0]和pulMsgRam[1]赋值的。 我猜测,这个地址应该是cpu1和cpu2都能够访问的。 所以cpu1做了赋值,cpu2就不用再赋值了。而且cpu2它并不知道在cpu1的程序中给结构体cpu2tocpu1分配的地址是多少。
3.4 ipcget
void ipcget(void){
// Give the shared memory access permission to CPU1
IPCReqMemAccess(&g_sIpcController2, GS0_ACCESS, IPC_GSX_CPU1_MASTER, ENABLE_BLOCKING);
// Inform CPU1 that I want to read the data
IPCLtoRBlockRead(&g_sIpcController2, pulMsgRam[0], (uint32_t)pusCPU01BufferPt, 256, ENABLE_BLOCKING,IPC_FLAG17);
// Wait until read data is ready (by checking IPC Response Flag is cleared).
while(IpcRegs.IPCFLG.bit.IPC17){};
// Transfer the read data from the shared memory to the local structure
memcpy((void *)&cpu1tocpu2.data[0].s[0],(void *)pusCPU01BufferPt,sizeof(struct ipcstruct));
}
ipcget也只有4行程序,下面进行逐行解析
IPCReqMemAccess(&g_sIpcController2, GS0_ACCESS, IPC_GSX_CPU1_MASTER, ENABLE_BLOCKING);
第一行的作用是把共享内存的访问权限 释放给 cpu1 (支持数据块操作的访问权限),因为我们发送的时候占用这个权限,所以也得我们来释放。cpu1也不知道你cpu2什么时候不用共享内存的。
IPCLtoRBlockRead(&g_sIpcController2, pulMsgRam[0], (uint32_t)pusCPU01BufferPt, 256, ENABLE_BLOCKING,IPC_FLAG17);
第二行的这个函数,也是ti官方提供的,也是可以在example程序里面找到的,其作用就是 通知cpu1,我要读取你 pulMsgRam[0] 这个地址里面存放的数据块, 我要读256个数据单元(16位),你收到我的消息后,你把数据,给我放在共享内存的地址 pusCPU01BufferPt(共享内存起始地址)中。你如果搞定了,你就通过IPC_FLAG17来通知我,你搞定了。
这个函数运行结束后,cpu1就会触发中断函数CPU02toCPU01IPC1IntHandler,里面的
case IPC_BLOCK_READ:
IPCRtoLBlockRead(&sMessage);
break;
我们不需要在这个地方添加其他的操作,IPCRtoLBlockRead这个函数,会去完成cpu2想要的操作(就是把cpu2要从cpu1读取的数据,放到对应的共享内存里面去)。这个case执行完后,IPC_FLAG17会被清零。
while(IpcRegs.IPCFLG.bit.IPC17){};
第三行作用是等待cpu1完成cpu2的读取数据请求,cpu1完成操作后,IpcRegs.IPCFLG.bit.IPC17会被清零,也就会跳出循环。
memcpy((void *)&cpu1tocpu2.data[0].s[0],(void *)pusCPU01BufferPt,sizeof(struct ipcstruct));
第四行是不是似曾相识, 刚刚ipcsend发送是把结构体的数据拷贝到共享内存。 而现在是把共享内存的数据拷给的结构体中。这个操作结束之后,我们就可以直接使用cpu1tocpu2这个结构体了。
3.5 CPU2的IPC中断处理函数
// Normally, it will never be called
__interrupt void CPU01toCPU02IPC0IntHandler (void){
tIpcMessage sMessage;
while(IpcGet(&g_sIpcController1, &sMessage,DISABLE_BLOCKING) != STATUS_FAIL){
switch (sMessage.ulcommand)
{
case IPC_DATA_WRITE:
IPCRtoLDataWrite(&sMessage);
break;
default:
break;
}
}
IpcRegs.IPCACK.bit.IPC0 = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}
// Normally, it will never be called
__interrupt void CPU01toCPU02IPC1IntHandler(void){
IpcRegs.IPCACK.bit.IPC1 = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}
可以看到,CPU2的IPC中断处理函数基本上啥都没写。 为什么? 因为所有的操作都是从cpu2向cpu1发起的,只会在cpu1触发中断。而cpu1没有向cpu2发送中断请求,所以不会在cpu2触发中断。因此cpu2的中断处理函数可以草率一点,不用写什么。
那么至此,TMS320F28377D最简单最高效的IPC核间通信已经讲完了。 把所有提及的内容组合到一起就是完成的IPC核间通信了,值得注意的是,ipcconfig和ipcinit只会被执行一次。 CPU1应该以某种固定的频率去给cpu1tocpu2结构体赋值。 CPU2也得也固定的频率去调用ipcsend和ipcget函数。