[TI] 以C6678为例的多核DSP通信方法总结

1. 写在前面

最近忙于使用多核 DSP 进行开发,型号为 FTM6678。但这款 DSP 和 TI 的 C6678 相似,甚至于很多功能都相同。开发过程中涉及到了很多数据的通信,包括核内通信与核间通信。因此本文拟对这些通信方法进行一个大致的描述。

2. 核内通信

完成核内通信需要使用到 SYS/BIOS 模块。概念参考:TI-RTOS Kernel (SYS/BIOS) User's Guide 的 Chatper4。<span style="color:red;font-weight:bold;">强烈建议去阅读手册,本文只是提供了一个方法的概述。

核内通信用于在单核之间进行通信。在使用 c 语言开发的过程中,核内通信可以帮助完成多个 .c 文件或者任务的数据交互与同步。

下表展示了核内通信的模块及其简短描述:

Module Description
Semaphore 用于任务间同步和互斥的信号量。
Event 线程间通信和同步的事件机制。
Gates 防止并发访问临界区的锁。
Mailboxes 任务间消息传递的机制。
Queues SYS/BIOS 中队列的实现。

2.1. Semaphore

又称信号量,与操作系统中的信号灯功能一致,可以进行任务间的同步和互斥。信号量对象是一个非负数,其创建分为静态创建和动态创建两种形式,下述以最简单的二进制信号量为例描述创建过程,更具体的可查看手册解决。

2.1.1. 静态创建

静态创建一般都在 .cfg 文件下创建。以下代码创建了一个名为 hSem 的、初值为 0 的二进制信号量:

var Semaphore = xdc.useModule('ti.sysbios.knl.Semaphore');  // Necessary include
var SemParam = new Semaphore.Params;
SemParam.instance.name = "hSem";
SemParam.mode = Semaphore.Mode_BINARY;
Program.global.hSem = Semaphore.create(0, SemParam);        // Initialize to 0

2.1.2. 动态创建

动态创建一般都在 .c 或 .cpp 文件下创建。以下代码创建了同样的 hSem 信号量。

#include <ti/sysbios/knl/Semaphore.h>       // Must be included

Semaphore_Handle hSem;
Semaphore_Params semParams;
Semaphore_Params_init(&semParams);          // Necessary
semParams.mode = Semaphore_Mode_BINARY;
hSem = Semaphore_create(0, &semParams, NULL);

2.1.3. 等待和释放信号量

Semaphore_post 相当于 Linux 操作系统中的 V 原语,释放一个信号量资源;Semaphore_pend 相当于 Linux 操作系统中的 P 原语,申请一个信号量资源。

Void Semaphore_post(Semaphore_Handle sem);                // Signal a semaphore
Bool Semaphore_pend(Semaphore_Handle sem, UInt timeout);  // Wait for a semaphore

2.2. Event

Events(事件)提供了在线程中通信和同步的方法,类似于信号量,但是事件允许你指定多个条件(即“事件”),这些条件必须全部满足后,等待的线程才会被唤醒并继续执行。

2.2.1. 静态创建

在 .cfg 下创建:

var Event = xdc.useModule("ti.sysbios.knl.Event");
Program.global.myEvent = Event.create();

2.2.2. 动态创建

在 .c 或 .cpp 文件下创建:

Event_Handle myEvent;
Error_Block eb;
...
Error_init(&eb);
/* Default instance configuration params */
myEvent = Event_create(NULL, &eb);
if (myEvent == NULL) {
    System_abort("Event create failed");
}

2.2.3. 等待和释放事件

事件同样使用 pendpost 来进行相应操作,不同的是必须指定具体操作的是哪个事件。

UInt Event_pend(Event_Handle event, UInt andMask, UInt orMask, UInt timeout);
Void Event_post(Event_Handle event, UInt eventIds);

讲解一下 andMask 以及 orMask:前者代表“全部必须发生”的事件位掩码,后者代表“任一发生即可”的事件位掩码。

注意:只有任务才能 pend;软硬件中断及任务均可 post

可以和后续要介绍的 Mailboxes 联动,从而避免 CPU 空转。

2.3. Gates

Gates(门,又称锁)是用来防止并发访问临界区的手段,对应 Linux 操作系统中的锁。如果一段代码正在操作全局变量、共享缓冲区、硬件寄存器等“公共资源”,Gates 可以保证它一次性做完、不被打断。

2.3.1. 基于抢占式的锁实现

三种锁,按关闭的抢占源从“最狠”到“最轻”排列:

GateHwi:
关/开 硬件中断(整个 CPU 屏蔽中断)。
任何线程(Task、Swi、Hwi)都能用,但会把系统中断延迟压到最小,所以临界区必须极短。

GateSwi:
关/开 软件中断(Swi 被禁止抢占)。
只允许 Task 和 Swi 使用,Hwi 里不能用。比 GateHwi 轻,中断仍能响应。

GateTask:
关/开 任务调度(Task 禁止抢占)。
只能在 Task 环境里用,Hwi/Swi 里禁止调用。最轻量级,只防其它 Task 抢 CPU。

2.3.2. 基于信号量的锁实现

GateMutex:
用 二进制信号量 当锁。
可能会阻塞(拿不到锁就挂起)。
所以只能用于 Task,绝对不能在 Swi 或 Hwi 里用(它们不能阻塞)。
功能:保证只有一个 Task 能进临界区。

GateMutexPri:
是带优先级继承的互斥锁。
解决 优先级反转 问题。
优先级继承:低优先级 Task 临时“升级”成高优先级,尽快放锁。
同样只能用于 Task,不能用于 Swi/Hwi。

这里举个例子说明优先级反转:低优先级的 Task 先拿到了锁。此时高优先级任务被调度,同时高优先级任务也需要获得锁资源,但锁资源被低优先级持有,故高优先级需要等待低优先级使用完成。此时又来了一个中优先级的任务,它不需要锁资源,故可以直接抢占 CPU 。此时高优先级任务便需要等待中、低优先级两个任务。

2.3.3. 动态创建

以 GateHwi 为例:

UInt gateKey;
GateHwi_Handle gateHwi;
GateHwi_Params prms;
Error_Block eb;

...

Error_init(&eb);
GateHwi_Params_init(&prms);
gateHwi = GateHwi_create(&prms, &eb);
if (gateHwi == NULL) {
    System_abort("Gate create failed");
}

2.3.4. 加锁和开锁

一个简单的加锁示例。

/* Simultaneous operations on a global variable by multiple
 * threads could cause problems, so modifications to the global
 * variable are protected with a Gate. */
gateKey = GateHwi_enter(gateHwi);
myGlobalVar = 7;
GateHwi_leave(gateHwi, gateKey);

2.4. Mailboxes

又称邮箱,可用于将缓冲区从一个任务传递到同一处理器上的另一个任务。一个邮箱实例可以被多个读取器和写入器使用。Mailboxes 会将缓冲区中的内容复制到固定大小的内部缓冲区中(内部缓冲区是在创建实例时事先定义的)。

写入时数据从本地缓冲区复制到内部缓冲区,读取时数据从内部缓冲区复制到本地缓冲区。所以可以认为 Mailboxes 传递的是数据副本。

Mailboxes 可以对数据传输进行流量控制。

2.4.1. 创建与删除

可以在 .c 或者 .cpp 中调用以下函数,也可以通过 .cfg 文件执行相关操作。

Mailbox_Handle Mailbox_create(SizeT bufsize, 
                              UInt numBufs, 
                              Mailbox_Params *params,
                              Error_Block *eb)

Void Mailbox_delete(Mailbox_Handle *handle);

2.4.2. 写入和读取

写入使用 Mailbox_post() :

Bool Mailbox_post(Mailbox_Handle handle, 
                  Ptr buf, 
                  UInt timeout);

读取使用 Mailbox_pend() :

Bool Mailbox_pend(Mailbox_Handle handle, 
                  Ptr buf, 
                  UInt timeout);

2.4.3. 补充

通过配置 readerEvent、readerEventId、writerEvent、writerEventId 可以将 Mailboxes 和 Event 联动,使得 Mailboxes 在等待读取/写入的同时,还能处理其它不同的事件。Event_pend() 的多路阻塞能力能避免 CPU 空转。

具体示例详见 TI-RTOS Kernel (SYS/BIOS) User's Guide 的 Section 4.2.1。

2.5. Queues

又称队列。Queues 在 SYS/BIOS 中是用双向链表来实现的,这样的好处是没有最大大小的限制,同时可以在任意地方插入或删除元素。

2.5.1. 动态创建

可以在 .c 或者 .cpp 中调用以下函数。

/* This structure can be added to a Queue because the first field is a Queue_Elem. */
typedef struct Rec {
    Queue_Elem elem;
    Int data;
} Rec;

Rec r1, r2;
Queue_Handle myQ;

r1.data = 100;
r2.data = 200;

// No parameters or Error block are needed to create a Queue.
myQ = Queue_create(NULL, NULL);

2.5.2. 写入和读取

使用 Queue_enqueue() 来进行写入操作:

// Add r1 and r2 to the back of myQ.
Queue_enqueue(myQ, &(r1.elem));
Queue_enqueue(myQ, &(r2.elem));

使用 Queue_dequeue() 来进行读取操作:

Rec* rp;
// Dequeue the records and print their data
while (!Queue_empty(myQ)) {
    // Implicit cast from (Queue_Elem *) to (Rec *)
    rp = Queue_dequeue(myQ);
    System_printf("rec: %d\n", rp->data);
}

2.5.3. 遍历、插入和移除

遍历队列:

// Queue_next() and Queue_prev() return the next and previous elements in a Queue, respectively. 
Queue_Elem *elem;
for (elem = Queue_head(myQ); elem != (Queue_Elem *)myQ; elem = Queue_next(elem)) {
    ...
}

插入和移除元素:

Queue_enqueue(myQ, &(r1.elem));
/* Insert r2 in front of r1 in the Queue. */
Queue_insert(&(r1.elem), &(r2.elem));
/* Remove r1 from the Queue. Note that Queue_remove() does not 
 * require a handle to myQ. */
Queue_remove(&(r1.elem));

2.5.4. 补充

Queue 提供了两种原子 API 操作,可以在操作队列的时候禁用中断。

Queue_get() 是 Queue_dequeue() 的原子操作,而 Queue_put() 是 Queue_enqueue() 的原子操作。

3. 核间通信

完成核间通信需要使用到 IPC 模块。概念参考:SYS/BIOS Inter-Processor Communication (IPC) User’s Guide 的 Chatper2。<span style="color:red;font-weight:bold;">强烈建议去阅读手册,本文只是提供了一个方法的概述。

核间通信就是多核 CPU 之间互相传数据、做同步的一套机制。

下表展示了核间通信的模块及其简短描述:

3.1. IPC Module

IPC 模块包含初始化和通信模块。所有使用到 IPC 模块的核都得调用 Ipc_start() 这一个函数。该函数将完成 IPC 模块的初始化,并同步所有活跃的核。

在调用 IPC 模块中的通信模块前需要在 main 函数中调用 Ipc_start()。Ipc_start() 会在 SharedRegion 0 创建资源管理表供其它 IPC 模块使用,所以 SharedRegion 0 必须对所有核可访问。

3.1.1. 配置

相关的配置需要在 .cfg 中进行。

Ipc = xdc.useModule('ti.sdo.ipc.Ipc');
// Configure how the IPC module synchronizes processors 
Ipc.procSync = Ipc.ProcSync_ALL;

当设置 procSync 为 Ipc.ProcSync_ALL 时,所有调用 Ipc_start() 的核都会自动连接并完成同步。一般都使用这种,比较省事。
当设置为 Ipc.ProcSync_PAIR 时,需要使用 Ipc_attach() 显式连接所需的核。这种状态下可以自定义同步的顺序,以及节省资源。
当设置为 Ipc.ProcSync_NONE 时,Ipc_start() 在设置其他模块所需的对象之前不会同步任何处理器。这需要谨慎使用。

注意:
Ipc_attach() 需要是双向调用的,且 ID 大的核必须得等 ID 小的核对自身发起 Ipc_attach(),自身才能对其发起 Ipc_attach()。如处理器 2 只能在处理器 1 调用 Ipc_attach(2) 之后才能调用Ipc_attach(1)。

可以配置一个函数,使其在附加到远程处理器或从远程处理器分离时,除了执行默认操作外,还执行自定义操作。函数必须是非阻塞且能执行完成的。这些函数分别在 Ipc_attach() 的末尾和 Ipc_detach() 的开头运行。

var Ipc = xdc.useModule('ti.sdo.ipc.Ipc');
var fxn = new Ipc.UserFxn;
fxn.attach = '&userAttachFxn1';     // after attach, userAttachFxn1 is called. 
fxn.detach = '&userDetachFxn1';     // before detach, userDetachFxn1 is called. 
Ipc.addUserFxn(fxn, 0x1);
fxn.attach = '&userAttachFxn2';
fxn.detach = '&userDetachFxn2';
Ipc.addUserFxn(fxn, 0x2);

3.2. MultiProc

MultiProc 是 IPC 框架中的处理器 ID 管理模块,用于在多核/多处理器环境中唯一标识和识别各个处理器。该模块为上层通信组件(如 MessageQ、Notify、HeapBufMP、ListMP 等)提供统一的处理器寻址抽象,使应用层无需关心底层硬件核心编号、物理地址映射或启动顺序,即可实现跨核资源访问与消息传递。

为简便,这里只讨论同一个 cluster(处理器集群)下的方法和配置。多 cluster 的配置可以看官方手册。

3.2.1. 配置

分为静态配置和动态配置。

静态配置,适用于处理器角色固定、启动顺序确定的系统,比较常用。以某一个核的.cfg文件为例:

// config.bld 或 .cfg 配置文件
var MultiProc = xdc.useModule('ti.sdo.utils.MultiProc');

// 当前处理器名为 "DSP",Cluster 包含 ["VIDEO", "DSS", "DSP"]
// 自动分配:VIDEO=0, DSS=1, DSP=2
MultiProc.setConfig("DSP", ["VIDEO", "DSS", "DSP"]);

动态配置,运行时确定本地 ID:

// 配置阶段:仅定义 Cluster 成员,不指定本地名称
MultiProc.setConfig(null, ["CORE0", "CORE1", "CORE2"]);

// 启动早期(Ipc_start() 之前):根据硬件/协议设置本地 ID
UInt16 localId = getProcIdFromHardware();  // 用户自定义函数
MultiProc_setLocalId(localId);

3.3. MessageQ

又称消息队列。可以用来结构化传输和接收可变长度的消息。对于一个 MessageQ 对象,当创建时,仅有一个接收端,但可有多个发送端。

3.3.1. 一些关键特征

  • 透明迁移:写/读线程可无感迁移至其他处理器,无需修改运行时代码;
  • 超时控制:接收消息时可指定超时时间;
  • 双向通信:接收端可识别发送端并直接回送消息;
  • 零拷贝:支持消息零拷贝传递,减少数据复制开销;
  • 任意队列:消息可存放在任意消息队列,灵活路由;
  • 全线程友好:发送与接收可在任意线程类型(任务、中断、线程池等)中完成;
  • 自定义通知:通知机制(信号量、回调、事件标志等)由应用决定,适应不同场景;
  • QoS 池化:可按消息队列分配不同缓冲区池,实现服务质量分级与资源隔离。

3.3.2. 接收与发送

接收端可以调用以下 API:

// To create a messageQ object
MessageQ_create();
// Free message allocation
MessageQ_free(); 
// Get a message
MessageQ_get();
// Delete the MessageQ object
MessageQ_delete();

发送端可以调用以下 API:

// Open the MessageQ
MessageQ_open();
// For message allocation
MessageQ_alloc();
// Put a message
MessageQ_put();
// Close the MessageQ
MessageQ_close();

3.3.3. 配置与创建

配置在 .cfg 中配置:

var MessageQ = xdc.useModule('ti.sdo.ipc.MessageQ');
/* 
Module-wide configuration properties you can set are as follows. 
The default values are shown in the following statements. 
See the IPC online documentation for details. 
*/
// Maximum length of MessageQ names 
MessageQ.maxNameLen = 32;
// Max number of MessageQs that can be dynamically created 
MessageQ.maxRuntimeEntries = 10; 
// Number of heapIds in the system
MessageQ.numHeaps = 0;
// Section name used to place the names table 
MessageQ.tableSection = null; 

只能在 .c 或 .cpp 中动态创建实例化对象:

// Name must be supplied. An unnamed MessageQ can't be opened.
MessageQ_Handle MessageQ_create(String name, MessageQ_Params *params);
//  This function extracts the QueueId from the object.
UInt32 MessageQ_getQueueId(MessageQ_Handle handle);

// We usually create a MessageQ like this:
MessageQ_Handle messageQ = MessageQ_create(CORE0_MESSAGEQNAME, NULL);
// ID is very useful
MessageQ_QueueId QId_Core0 = MessageQ_getQueueId(messageQ);

3.3.4. 打开消息队列与分配空间

发送端需要打开接收端的消息队列才能向接收端发送消息。

// This function expects a name, which must match with the name of the created object.
Int MessageQ_open(String name, MessageQ_QueueId *queueId);


MessageQ_QueueId QId_Core0 = MessageQ_getQueueId(messageQ);;
Int status;
/* Open the remote message queue. Spin until it is ready. */
do {
    status = MessageQ_open(CORE0_MESSAGEQNAME, &QId_Core0);
}
while (status < 0);

为消息分配空间,可使得消息在任意消息队列被发送。接收端接收后可进行消息复用或者释放。

MessageQ_Msg MessageQ_alloc(UInt16 heapId, UInt32 size); 

塞入消息队列的消息结构体,第一个元素一定得是 MessageQ_MsgHeader 结构,以 MyMsg 结构体为例:

typedef struct MyMsg {
    MessageQ_MsgHeader header;  // Required, and no modified
    SomeEnumType type           // Can be any field
    ...                         // ...
} MyMsg;

一般来说, 为消息分配的空间会被放在 MessageQ_registerHeap() 函数注册的堆里面。以 SHAREREGION_MSM_ID = 0 的 SharedRegion 共享堆区域为例,使用该函数在其中注册一个 MessageQ 用的堆。

Int MessageQ_registerHeap(Ptr heap, UInt16 heapId);

/* Register this heap with MessageQ */
#define MESGQ_SHAREREGION_MSM_ID (0)
Int status1 =  MessageQ_registerHeap(SharedRegion_getHeap(SHAREREGION_MSM_ID), 
                                     SHAREREGION_MSM_ID);

// Heaps can be unregistered via MessageQ_unregisterHeap().

应用程序可以使用多个堆来允许应用程序调节其消息使用。
例如,应用程序可以从快速片上内存堆中分配关键消息,从较慢的外部内存堆中分配非关键消息。此外,MessageQ使用的堆可以与其他模块和/或应用程序共享。

此外还有不在堆上分配消息空间的做法,参考手册的 2.3.4.2 小节,这里就不再说明了。

3.3.5. 发送与接收

当消息队列被打开,且消息所需空间已经被分配了,发送端便可用 MessageQ_put() 来往消息队列里送入消息。

Int MessageQ_put(MessageQ_QueueId queueId, MessageQ_Msg msg); 

可以使用 MessageQ_setMsgPri() 函数设置消息的优先级,有 3 个优先级。

MessageQ对象在内部维护两个链表:正常和高优先级。正常优先级消息以FIFO方式放置在“正常”链表上。高优先级消息以FIFO方式放置在“高优先级”链表上。紧急消息放置在高链表的开头:

// - MessageQ_NORMALPRI = 0
// - MessageQ_HIGHPRI = 1
// - MessageQ_URGENTPRI = 3
Void MessageQ_setMsgPri(MessageQ_Msg msg,
                        MessageQ_Priority priority)

接收端可以使用 MessageQ_get() 来获取消息,如果无消息,则会按设定的参数阻塞或跳过:

Int MessageQ_get(MessageQ_Handle handle, 
                 MessageQ_Msg *msg,
                 UInt timeout)

3.4. ListMP

基于链表列表的模块。ListMP 使用共享内存为多个处理器提供一种共享、传递或存储数据缓冲区、消息或状态信息的方式。

ListMP 使用多处理器门来防止多个处理器同时访问同一链表。所有 ListMP 操作在处理器之间都是原子的。

3.4.1. 动态创建与访问

以下是在 .c 或 .cpp 文件中创建 ListMP 的实例:

ListMP_Params params;
GateMP_Handle gateHandle;
ListMP_Handle handle1;

/* If gateHandle is NULL, the default remote gate will be
 automatically chosen by ListMP */
gateHandle = GateMP_getDefaultRemote();
ListMP_Params_init(&params);
params.gate = gateHandle;
params.name = "myListMP";
params.regionId = 1;        // shared_region ID
handle1 = ListMP_create(&params, NULL);

创建 ListMP 实例的处理器必须根据其本地地址空间指定共享内存。此共享内存必须已由应用程序在 SharedRegion 模块中定义。

创建后可使用下列函数访问实例,ListMP_open() 仅在启用全局中断时调用:

while (ListMP_open("myListMP", &handle1, NULL) < 0) {
    ;
}

注意:
要从链表中添加/删除的 ListMP 元素必须存储在共享内存区域中。
链表必须位于共享列表的所有处理器的最坏情况高速缓存线边界上。

3.4.2. 其它常用 API

注意,要放入链表的结构体,其第一个元素必须是 ListMP_Elem 类型的字段,如:

typedef struct Tester {
    ListMP_Elem elem;
    Int scratch[30];
    Int flag;
} Tester;

常用的 API,包括判空、取 / 放数据等如下所示,此处省略了输入参数:

// 测试 ListMP 是否为空
ListMP_empty()

// 取出并返回 ListMP 头部元素
ListMP_getHead()

// 取出并返回 ListMP 尾部元素
ListMP_getTail()

// 在当前位置插入元素
ListMP_insert()

// 返回 ListMP 中下一个元素(非原子操作)
ListMP_next()

// 返回 ListMP 中前一个元素(非原子操作)
ListMP_prev()

// 将元素压入 ListMP 头部
ListMP_putHead()

// 将元素压入 ListMP 尾部
ListMP_putTail()

// 从 ListMP 中间移除当前元素
ListMP_remove()

3.5. Heap*MP

Heap*MP 就是“多核共享内存堆”——让不同核都能安全地申请 / 释放同一块共享内存。共享内存堆一共有一下三种实现:

HeapBufMP:
固定大小块管理;一个实例只服务单一块尺寸,可多实例搭配不同尺寸。

HeapMultiBufMP:
“多桶”固定大小管理;一个实例支持最多 8 种块尺寸,自动选最小够用桶,桶空即失败。

HeapMemMP:
传统可变长堆;在一段连续共享内存里按需分配、释放任意大小的块。

3.5.1. 创建

以下示例均以 HeapBufMP 为例:

// 初始化参数结构体
HeapBufMP_Params_init(&heapBufMPParams);

// 指定共享内存区域索引(0 表示默认区域)
heapBufMPParams.regionId = 0;

// 为 NameServer 设置堆实例名称
heapBufMPParams.name = "myHeap";

// 每个块的对齐字节数(256 字节对齐)
heapBufMPParams.align = 256;

// 固定大小块的总数量
heapBufMPParams.numBlocks = 40;

// 单个块的字节大小
heapBufMPParams.blockSize = 1024;

// 使用系统默认门锁(NULL 表示默认)
heapBufMPParams.gate = NULL;

// 创建 HeapBufMP 实例
heapHandle = HeapBufMP_create(&heapBufMPParams);

// 将 HeapBufMP 注册到 MessageQ,供跨核消息分配使用
MessageQ_registerHeap(HeapBufMP_Handle_upCast(heapHandle), HEAPID);

3.5.2. 其它常用 API

以下示例均以 HeapBufMP 为例:

// 打开一个 HeapBufMP 实例
/* Open heap created by other processor. Loop until open. */
do {
    status = HeapBufMP_open("myHeap", &heapHandle);
}
while (status < 0);

// 关闭一个实例
Int HeapBufMP_close(HeapBufMP_Handle *handlePtr);

// 删去一个实例
Int HeapBufMP_delete(HeapBufMP_Handle *handlePtr);

// 申请空间
Void* HeapBufMP_alloc(HeapBufMP_Handle handle, SizeT size, SizeT align);

// 释放空间
Void HeapBufMP_free(HeapBufMP_Handle handle, Ptr block, SizeT size);

// 查询 heap 状态
Void HeapBufMP_getExtendedStats(HeapBufMP_Handle handle, HeapBufMP_ExtendedStats *stats);
Void HeapBufMP_getStats(HeapBufMP_Handle handle, Ptr stats);

3.6. GateMP

GateMP 实例可以用于强制执行本地和远程上下文保护。也就是说,进入 GateMP 可以防止在同一处理器上运行的另一个线程的抢占,同时防止远程处理器进入同一闸门。GateMP 通常用于保护对共享资源的读/写,例如共享内存。

3.6.1. 动态创建

只能动态创建 GateMP.

GateMP_Params gparams;
GateMP_Handle gateHandle;
...
GateMP_Params_init(&gparams);
// localProtect 参数用来配置门对本地处理器的保护方式
gparams.localProtect = GateMP_LocalProtect_THREAD;
// remoteProtect 参数用来配置门对远程处理器的保护方式
gparams.remoteProtect = GateMP_RemoteProtect_SYSTEM;
gparams.name = "myGate";
// regionId 指示使用哪一个 shared_region
gparams.regionId = 1;
gateHandle = GateMP_create(&gparams, NULL);

3.6.2. 其它常用 API

以下展示了常用的 API 操作,此处省略输入参数:

// 打开一个实例
status = GateMP_open("myGate", &gateHandle);
if (status < 0) {
    System_printf("GateMP_open failed\n");
}

// 关闭一个实例
GateMP_close();

// 删去一个实例
GateMP_delete();

// 获得锁 
GateMP_enter();

// 释放锁
GateMP_leave();

// 查询锁状态
GateMP_query();

3.6.3. 补充

其它核间通信模块也可使用 GateMP 来保护自己的资源。

3.7. Notify

Notify 用于接受通知并产生对应响应。

接受通知的核可注册相应回调函数,并通过为回调函数注册事件来响应对应的通知。

3.7.1. 注册回调函数

回调函数的注册必须按照以下结构:

/* 
procId:核的逻辑id
lineId:中断线编号,通常为0
eventId:事件id,用来区分事件
arg:可用来传参
payload:4字节小数据
*/
Void cbFxn(UInt16 procId, UInt16 lineId, UInt32 eventId, UArg arg, UInt32 payload);

3.7.2. 将回调函数绑定到事件上

一般使用以下 API 完成绑定:

// 将一个或多个回调函数绑定到事件上
// 若需要多事件绑定,只需重复调用
Int Notify_registerEvent(UInt16 procId,
                        UInt16 	lineId,
                        UInt32 	eventId,
            Notify_FnNotifyCbck fnNotifyCbck,
                        UArg 	cbckArg 
);

// 将一个回调函数绑定到事件上
Int Notify_registerEventSingle(UInt16 procId,
                            UInt16 lineId,
                            UInt32 eventId,
            Notify_FnNotifyCbck fnNotifyCbck,
                            UArg cbckArg 
);



// 示例:
Int status;
armProcId = MultiProc_getId("ARM");
Ipc_start();
/* Register cbFxn with Notify. It will be called when ARM
 * sends event number EVENTID to line #0 on this processor.
 * The argument 0x1010 is passed to the callback function. */
status = Notify_registerEvent(armProcId, 0, EVENTID,
           (Notify_FnNotifyCbck)cbFxn, 0x1010);
if (status < 0) {
    System_abort("Notify_registerEvent failed\n");
}

3.7.3. 远端核发送

远端核可以发送相应的通知(事件)来触发上述定义的回调函数。

使用下述 API:

/*
waitClear参数为TRUE表示:如果已发送过相同事件ID的先前事件,则Notify驱动程序会等待确认先前事件已接收。
*/
Int Notify_sendEvent(UInt16 procId,
                    UInt16 lineId,
                    UInt32 eventId,
                    UInt32 payload,
                    Bool waitClear  
);

3.7.4. 补充

特定事件可通过调用 Notify_disableEvent() 和 Notify_enableEvent() 函数进行禁用或启用。

3.8. SharedRegion

SharedRegion 模块会创建共享内存区域查找表。查找表包含核对系统中每个共享区域的视图。如果某个核无法查看某个共享内存区域,则该共享内存区域对该核无效。每个核都有自己的查找表,存放在本地的内存中。

3.8.1. 本地查找表

一般在 .cfg 中配置 SharedRegion 的查找表。

字段 类型 含义
base Ptr 该区域在本核视角下的基地址(物理或映射后虚拟地址),不同的核可能不一样。
len SizeT 区域总长度,所有核必须填同一值
ownerProcId UInt16 负责管理该区域的 MultiProc ID;owner 会创建 HeapMemMP,其余核只 open
isValid Bool 本核能否访问;TRUE=可见,FALSE=不可见。
cacheEnable Bool 本核对该区域是否使能 cache。
cacheLineSize UInt16 区域对应的 cache line 大小,全系统必须相同
createHeap Bool 是否在该区域自动创建 HeapMemMP 堆。
name String 区域名字,仅调试/日志使用。

所有字段均存于本地私有内存

此外,属性 SharedRegion.numEntries 表示系统最多能支持多少个核来访问共享区域。只能在 cfg 脚本或 .cfg 文件里改,例如:SharedRegion.numEntries = 4。所有配置 SharedRegion 的核对于该属性的赋值都需保持一致。

注意事项:

  • SharedRegion.numEntries属性在所有处理器上必须相同。
  • SharedRegion 指针的大小为32位宽。
  • SharedRegion 查找表必须至少包含1个条目,才能进行地址转换。
  • 从单个处理器的角度来看,共享内存区域不得相互重叠。
  • IPC_start() 使用索引为 0 的 SharedRegion 创建资源管理表,供其他IPC模块内部使用。因此,SharedRegion 0 必须可供所有处理器访问。应用程序也可以使用 SharedRegion 0 ,但必须注意内存限制。

以下展示了在 .cfg 中配置 SharedRegion 的示例:

var SharedRegion = xdc.useModule('ti.sdo.ipc.SharedRegion');
SharedRegion.cacheLineSize = 32;
SharedRegion.numEntries = 4;
SharedRegion.translate = true;

var SHAREDMEM      = 0x0C000000;
var SHAREDMEMSIZE  = 0x00200000;

SharedRegion.setEntryMeta(0,
    {
        base: SHAREDMEM, 
        len: SHAREDMEMSIZE,
        ownerProcId: 0,
        isValid: true,
        cacheEnable: true,
        cacheLineSize: 128,
        createHeap: true,
        name: "internal_shared_mem"   
    }
);

// 如果 createHeap 字段设置为 true,则会在 SharedRegion 中创建 HeapMemMP 实例。

动态配置查找表操作不常用,这里就不展开叙述了。

3.8.2. 分配 Shared Region 中的内存空间

由于 SharedRegion 0 对所有核可见,故可以在上面申请内存空间。如下所示:

buf = Memory_alloc(SharedRegion_getHeap(0), sizeof(Tester) * COUNT, 128, NULL);

3.8.3. 获取关于 Shared Region 的信息

以下示例展示了如何获取相关信息:

/* 1. 先把地址转成 32 位“通用指针” */
SharedRegion_SRPtr srptr;
UInt16 id;

id   = SharedRegion_getId(addr);          /* 查这块地址属于哪个共享区 */
srptr = SharedRegion_getSRPtr(addr, id);  /* 拿到 32 位通用指针 */
addr  = SharedRegion_getPtr(srptr);       /* 把通用指针再换回本地地址 */

/* 2. 其余常用 API */
id = SharedRegion_getIdByName("MyRegion");    /* 用名字查id */

IHeap_Handle heap = SharedRegion_getHeap(id); /* 拿到这块区的堆句柄 */
Ptr buf = Memory_alloc(heap, 1024, 0, NULL);  /* 就能在共享区里申请内存 */
Memory_free(heap, buf, 1024);

Bool cacheOn  = SharedRegion_isCacheEnabled(id);    /* 查本区是否开缓存 */
UInt16 clSize = SharedRegion_getCacheLineSize(id);  /* 拿缓存行大小,方便 flush */
posted @ 2026-04-08 10:59  珞喻路小白  阅读(184)  评论(0)    收藏  举报