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 INT0PieCtrlRegs.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()// functionbreak;case IPC_CLEAR_BITS_PROTECTED:IPCRtoLClearBits_Protected(&sMessage);     // Processes// IPCReqMemAccess()// functionbreak;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 INT0PieCtrlRegs.PIEIER1.bit.INTx14 = 1;    // CPU2 to CPU1 INT1while(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 memoryIPCReqMemAccess(&g_sIpcController2, GS0_ACCESS, IPC_GSX_CPU2_MASTER, ENABLE_BLOCKING);// Waiting for accesswhile(MemCfgRegs.GSxMSEL.bit.MSEL_GS0 != 1U){};// Copy the data sent by CPU2 to CPU1 to the shared memorymemcpy((void *)pusCPU02BufferPt,(void *)&cpu2tocpu1.data[0].s[0],sizeof(struct  ipcstruct));//  Inform CPU1 that I have sent dataIPCLtoRBlockWrite(&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 CPU1IPCReqMemAccess(&g_sIpcController2, GS0_ACCESS, IPC_GSX_CPU1_MASTER, ENABLE_BLOCKING);// Inform CPU1 that I want to read the dataIPCLtoRBlockRead(&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 structurememcpy((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函数。

DSP_TMS320F28377D_最简洁最高效的IPC核间通信代码相关推荐

  1. soc的核间通信机制-->mailbox

    对于mailbox,这个东西其实看到了很多次,但是一直不知道是啥.这里大概看了一下,知道了为甚有这个玩意儿,以及这个玩意相关的有啥,至于具体怎么使用,以及详细的工作原因等着以后再说吧. 正文 目前很多 ...

  2. 多核异构核间通信-mailbox/RPMsg 介绍及实验

    1. 多核异构核间通信 由于MP157是一款多核异构的芯片,其中既包含的高性能的A7核及实时性强的M4内核,那么这两种处理器在工作时,怎么互相协调配合呢? 这就涉及到了核间通信的概念了. IPCC ( ...

  3. 用于多核DSP开发的核间通信

      TI的多核DSP以TMS320C6678为例,它是多核同构的处理器,内部是8个相同的C66x CorePac.对其中任意一个核的开发就和单核DSP开发的流程是类似的.   但是如果仅仅只是每个核独 ...

  4. 第十八节 多核异构核间通信–ipcc

    由于MP157 是一款多核异构的芯片,其中既包含的高性能的A7 核及实时性强的M4 内核,那么这两种处理器在工作时,怎么互相协调配合呢?这就涉及到了核间通信的概念了. IPCC (inter-proc ...

  5. 【SemiDrive源码分析】【MailBox核间通信】42 - 基于Mailbox 实现的 mailbox_demo 应用程序(RTOS Android侧通信实现)

    [SemiDrive源码分析][MailBox核间通信]42 - 基于Mailbox 实现的 mailbox_demo 应用程序(RTOS & Android侧通信实现) 一.编写RTOS侧 ...

  6. 【SemiDrive源码分析】【X9芯片启动流程】21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇

    [SemiDrive源码分析][X9芯片启动流程]21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇 一.Mailbox for Linux 驱动框架分 ...

  7. 【SemiDrive源码分析】【MailBox核间通信】43 - 基于Mailbox IPCC RPC 实现核间通信(代码实现篇)

    [SemiDrive源码分析][MailBox核间通信]43 - 基于Mailbox IPCC RPC 实现核间通信(代码实现篇) 一.RTOS侧 IPCC RPC 代码实现 二.Android侧 I ...

  8. 【SemiDrive源码分析】【X9芯片启动流程】20 - MailBox 核间通信机制介绍(代码分析篇)之 MailBox for RTOS 篇

    [SemiDrive源码分析][X9芯片启动流程]20 - MailBox 核间通信机制介绍(代码分析篇)之 MailBox for RTOS 篇 一.Mailbox for RTOS 源码分析 1. ...

  9. 【SemiDrive源码分析】【X9芯片启动流程】19 - MailBox 核间通信机制介绍(理论篇)

    [SemiDrive源码分析][X9芯片启动流程]19 - MailBox 核间通信机制介绍(理论篇) 一.核间通信 二.核间通信软件架构 三.Mailbox 设备驱动 3.1 Mailbox for ...

最新文章

  1. Spring源码分析【3】-SpingWebInitializer的加载
  2. JHM:原生动物对酸性矿山废水侵蚀土壤的生态响应机制
  3. 【CCNP考试】2010-01-31-北京-845(PASS)
  4. 【atcoder】Enclosed Points [abc136F]
  5. Ubuntu 14.04.5 imx6 开发环境搭建
  6. Bootstrap系列 -- 26. 下拉菜单标题
  7. android 8.0 调系统拍照_Android通知栏微技巧,8.0系统中通知栏的适配
  8. Android之getCacheDir()和getFilesDir()方法区别
  9. java程序设计图形题_面向对象与Java程序设计基础题目:设计一个程序可以一计算平面图形的面积和立体图形的体积。1.使用interface关键...
  10. jar包是什么意思_为什么越来越多的开发者选择使用Spring Boot?
  11. Automated Feature Engineering Basics
  12. LocalBroadcastManager分析
  13. 27. JavaScript Cookies
  14. 经济学有必要学python吗_学习经济学用啥软件
  15. php varbinary,MySQL 数据类型binary和varbinary的简单示例
  16. iOS越狱,插件afc2、afc2add、apple file conduit2的区别
  17. Github新手之路(全过程)(站在前辈的肩膀上的总结)
  18. PCB_焊盘工艺设计规范
  19. mysql 连续天数_mysql计算连续天数,mysql连续登录天数,连续天数统计
  20. 半乳糖修饰人血清白蛋白 Gal-HSA,Gal-PEG-HSA,单糖/多糖修饰蛋白等

热门文章

  1. Python中最常见括号()、[]、{}的区别
  2. Django项目实战 ----用户使用QQ登录
  3. Lambda 的语法
  4. BNO055数据读取之一:IIC
  5. mac mysql dmg安装_mac上面MYSQL安装
  6. 电脑关机word文件未保存的解决办法
  7. 文档识别软件力挺无纸化
  8. 福一中招聘计算机教师,北京市第一七一中学2019年招聘教师人员岗位表(第一批)...
  9. linux 系统命令被后门修改_红队实战攻防技术分享:Linux后门总结SSH利用篇
  10. tpproxy-tcp透明代理