Skip to main content
欢迎来到PAWPAW技术文档网站了解更多信息

快速入门

要编程一个XMOS设备,你可以使用C、C++或xC(带有多核扩展的C语言)。在你的软件项目中,你可以混合使用这三种类型的源文件。要使用xC,请将源文件的扩展名改为.xc,而不是.c。XTC编译器会自动检测到这个文件扩展名,并启用该文件的C扩展功能。

XTC工具提供了完全符合标准的C和C++编译(例如,以.c结尾的文件将被编译为标准C)。应用程序可以包含以xC和C混合编写的代码 - 你可以从标准C中调用以xC编写的函数,反之亦然。

Hello World

让我们从传统的“Hello World”程序开始:

#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}

这个程序在C和xC中完全相同。由于XMOS设备是嵌入式设备,只有在连接调试适配器(XTAG)时才会看到打印输出。在这种情况下,打印输出将通过调试适配器传输,并在运行程序时显示在XTC控制台上。

当您编译项目时,工具会跟踪您使用了哪些资源。例如,如果您使用-report选项编译此程序,您将得到以下信息输出:

Constraint check for "tile[0]" (node "0", tile 0):
Cores available: 8, used: 1. OKAY
Timers available: 10, used: 1. OKAY
Chanends available: 32, used: 0. OKAY
Memory available: 65536, used: 1176. OKAY
(Stack: 336, Code: 720, Data: 120)
Constraints checks PASSED.

您可以看到编译器准确地告诉您使用了多少内存(包括堆栈的使用量)。xC语言扩展已经设计成即使在并行运行使用相同内存空间的任务时,您也可以始终获取此信息。

并行运行

xC提供的C语言扩展的一个主要特性是能够并行运行代码。下面的程序同时运行三个执行线程,它们都打印出“Hello World”消息:

#include <stdio.h>
void hw(unsigned n) {
printf("Hello world from task number %u\n", n);
}
int main() {
par {
hw(0);
hw(1);
hw(3);
}
return 0;
}

其关键在于par结构,它可以在tile上并行运行多个任务。稍后将详细介绍此功能。

访问I/O硬件:LED闪烁示例

以下代码实现了一个简单的闪烁LED程序:

#include <platform.h>
#include <xs1.h>
#include <timer.h>
port p = XS1_PORT_1A;
int main() {
while (1) {
p <: 0;
delay_milliseconds(200);
p <: 1;
delay_milliseconds(200);
}
return 0;
}

该示例使用xC的port(端口)类型声明了一个名为p的端口,设置为1A端口(有关port的详细信息在I/O中描述)。<:运算符将一个值输出到端口。示例还使用了delay_milliseconds函数,该函数是XTC工具提供的库的一部分(位于头文件timer.h中)。

与C和C++的集成

在XMOS项目中,您可以同时使用C、C++和xC文件。它们将一起编译为相同的二进制文件。构建系统将根据文件的扩展名来进行编译。例如,如果一个项目具有以下目录结构:

app_my_project/
Makefile
src/
foo.c
bar.xc

foo.xc将作为xC编译,具有多核扩展功能,而bar.c将作为普通的C文件编译。然后,这两个目标文件将链接到同一个二进制文件中。

xC提供了对C的扩展,但不支持某些C特性。目前不支持的特性包括:

  • goto语句
  • 位域(bitfields)
  • 函数指针
  • C99风格的指定初始化器

从C编写的函数可以从xC中调用,反之亦然。由于xC是C的扩展,因此可以在xC中原型化C中的函数(除了使用上述不支持的语言特性的原型)。

例如,如果名为foo.c的文件包含以下函数:

int func() {
printf("This is a C function\n");
}

以及名为bar.xc的文件包含以下函数:

extern "C" {
extern int func(); // 此函数在C文件中定义
}

void g() {
par {
func();
func();
}
}

编译和链接这些文件时,xC函数g将调用C函数func

在从C调用xC时,xC中的一些新类型在C中是不可用的。然而,您可以使用C类型,在链接时将其转换为xC类型。

例如,可以将chanend传递给接受intunsigned int参数的C函数。与XTC套件一起提供的xccompat.h头文件包含了有用的宏和类型定义,这些宏和类型定义在每种语言中都会展开为正确的类型。

多核编程模型

执行并行任务

在xCORE设备上,程序由多个并行运行的任务组成。这些并发任务管理自己的状态和资源,并通过彼此之间的事务进行交互。系统的示例任务分解如图1所示。

image-20230705144312976

图1:任务之间的通信

任务的定义方式与任何C函数相同,例如:

void task1(int x, int a[20]) { ... }

函数定义没有特殊的关键字,任何函数都可以是一个任务。任务可以接受任何参数,但通常没有返回值。通常,任务不会返回,而是由一个无限循环组成:

void task1(...args...) {
//... 初始化 ...
while (1) {
//... 主循环 ...
}
}

任务在主程序的main函数中使用par结构进行调度以并行运行:

int main(void) {
par {
task1(... args ...);
task2(... args ...);
task3(... args ...);
}
}

在这里,您可以将参数传递给任务以配置它们。每个任务都将在xCORE设备的可用硬件上并行运行。

编译器会自动检查需要多少硬件资源,并在超出硬件资源限制时报错。编译器会自动为每个任务分配所需的堆栈和数据内存,并向您报告使用的总内存量(需要编译时开启-report选项)。

显式通信

任务之间始终使用显式的连接共享信息。这些连接允许任务之间进行同步事务,从而可以共享数据。

每个任务独立运行在自己的数据上。在某个时刻,其中一个任务将发起任务之间的事务同步。该任务将等待,直到另一个任务进入准备接受该事务的状态。此时,两个任务将交换一些数据,然后继续各自的工作。图2显示了事务同步的进行过程。

image-20230705150414869
图2:两个任务进行(数据)交换

共享内存访问

任务通过访问共享数据(例如全局变量)来共享数据,因此不使用其他同步方法(如锁、互斥量、信号量)。

两个任务可以通过向拥有数据的中间任务发出请求来共享公共数据。这样可以保持关注点的清晰分离。图3显示了这种共享数据的方法。

image-20230705151214670
图3:使用中间任务共享内存

采用这种方法可以明确地处理任务通信中可能出现的竞态条件。在效率方面,编译器以与直接共享内存相同的高效方式实现了这种方法。

异步通信

在这里描述的通信中,一个任务等待另一个任务准备好后才能继续事务交换,因此是同步的。

然而有时我们需要使用异步通信 - 一个任务希望向另一个任务发送数据而无需阻塞。任务可以继续执行其他工作,而发送的数据会被缓冲,直到目标任务准备好接收数据。

image-20230705151859460
图4:两个任务之间的通知

xC中有两种实现异步通信的方法。第一种方法是使用内置的通信方法,称为通知(notifications),如图4所示。这些通知允许一个任务在两个任务之间引发一个标志,表示希望进行通信。一旦通知被引发,任务可以继续执行,直到另一个任务发起回应通信。这类似于硬件通信总线中的硬件中断线返回到总线主控制器。

第二种实现异步通信的方法是在两个通信任务之间插入第三个任务,作为缓冲区,如图5所示。中间任务非常响应快,因为它只处理缓冲。一个任务可以通过“push”事务将数据放入缓冲区,然后继续执行。另一个任务可以异步地执行“pull”事务,从缓冲区中提取数据。

image-20230705152013970
图5:两个任务之间有一个共享内存FIFO的中间任务

这与在两个任务之间使用共享内存FIFO相同。

基于事件的编程

任务可以使用select语句对事件做出反应,该语句会暂停任务并等待事件发生。一个select语句可以等待多个事件,并处理首先发生的事件。

select语句的语法类似于C语言的switch语句:

select {
case event1:
// 处理事件
...
break;
case event2:
// 处理事件
...
break;
}

在实际运行中,这个语句会暂停,直到任何一个case(事件)发生,然后执行相应case中的代码。尽管select语句会等待多个事件,但在事件发生时只处理其中一个事件(break)。

通常,在编写其他微控制器的程序时,您的程序会通过中断来响应外部事件。当某个特定事件发生时(例如超时或外部I/O事件),将注册一个函数来处理中断。这个函数提供了一个中断服务例程(ISR)来处理事件。在xC中,不使用中断来处理事件,而是使用select构造提供了所需的功能。相当于ISR的是一个单独的任务,它执行一个select语句。

xC的方法有以下优势:

  • 与多核xCORE架构结合使用,可以大大提高对事件的响应速度。
  • 对最坏情况执行时间(WCET)进行推理会更容易,因为代码在执行过程中不会被中断。

完整的指定case的语法取决于事件的类型,并将在后续章节中进行描述。事件可以由其他任务发起事务定时器事件外部I/O事件引发。

底层硬件模型

了解目标硬件的底层结构能够帮助您理解xC中的编程方式。本节提供了硬件组织的描述。该描述给出了一个高级概述;有关特定XMOS设备的详细信息,请参阅设备的数据手册。

1
图6:底层硬件系统

图6显示了xC程序执行的硬件布局。系统由一个或多个由多个核心组成的 tile 组成。

什么是tile?

在芯片领域,"tile"这个术语的起源可以追溯到多核处理器架构。这种新型的多核处理器设计理念的主要核心,将处理器核心组织成一个网格状的结构。由于每个处理器核心都是相对独立的,通常具备独自的SRAM、IO等设备。处理器之间通过高速通道相连,类似于铺设在地板上的瓷砖(tile),因此得名为"tile"。这种结构便于扩展处理性能。

节点

每个物理封装被称为一个节点。一个网络可以是一个单一的节点,或者两个或更多的封装可以连接起来,它们之间可以使用称作xCONNECT的基础设施进行通信。每个节点都有自己的外部引脚,并且包含一个或多个tile,以及一些通信结构,用于在这些tile之间以及与网络上的其他节点(如果有的话)进行通信。

tile

系统被分割成 tile。每个 tile 包含一组硬件资源,具有以下内容:

  • 可以执行代码的多个核心
  • 参考时钟
  • 一些内存
  • 访问I/O子系统的能力

这个模型的一个关键概念是:只有在 tile 上执行的代码才能直接访问该 tile 的资源。

在XMOS设备上:

  • 每个 tile 有64KB到512KB的内存

    其中XS1设备(2009)有64KB,XCORE-200设备(2015)最多有256KB,而最新的XCORE.AI(2021)设备有512KB。

  • 每个 tile 上的内存没有缓存

  • 每个 tile 上的内存不会争用数据总线

    所有外设都通过不使用内存总线的I/O子系统实现;外设不支持直接内存访问(DMA)。

最后两个属性确保从内存加载或存储始终仅需要一个或两个指令周期。这使得对访问内存的代码执行最坏情况时间分析时,能够得到非常准确的结果。

位于不同 tile 上的任务不共享内存,但可以通过逻辑核间的channel(通道)进行通信。

在xC中,目标底层硬件平台提供了一组用于引用系统中 tile 的名称。这些名称在platform.h头文件中声明。通常会提供一个名为tile的数组。因此,可以将系统的 tile 表示为tile[0]tile[1]等。

核心

在一个 tile 内,核心(或逻辑核心)提供了一个独立的单元来执行代码。一个 tile 上的所有核心并行运行。

每个tile最高具有8个逻辑核。每个逻辑核都有自己的寄存器,并且独立于其他逻辑核执行指令。然而,一个tile内的所有逻辑核都共享该tile的资源和内存。

xCORE流水线有五个阶段,每个阶段需要一个系统时钟周期来完成。几乎每个xCORE指令都需要使用这个流水线来执行五个周期。这使得计算一段直线指令序列的持续时间变得简单。

Details

核心任务和流水线之间的关系图示 在下面的图示中,横轴表示时钟刻度,纵轴表示流水线的执行顺序,而其中上色的方块则是单个逻辑核对于任务的指令执行轨迹。

流水线图-单核
单逻辑核运行:每5个时钟刻度执行一次指令(单核主频为 f/5 MHz)

五个逻辑核可以并行运行,但是在流水线的使用上是错开的。以便在给定的时钟周期内,每个逻辑核使用不同的流水线阶段。这五个逻辑核将独立运行,并且每个逻辑核获得整个tile可用的MIPS(每秒机器指令数)的五分之一。

流水线图-五核
五个逻辑核运行:每5个时钟刻度执行一次指令(单核主频为 f/5 MHz)

当超过五个逻辑核处于活动状态时,每个逻辑核的执行速率将会下降,以在它们之间共享五级流水线。xcore硬件调度器使用循环的方式来分配每个逻辑核的时间片(也称为时间片轮转调度)。

流水线图-八核
八个逻辑核运行:每8个时钟刻度执行一次指令(单核主频为 f/8 MHz)
提示

以XCORE.AI架构的XU316-1024-QF60B-PP24为例,该封装单个tile的主频为600MHz。

当在单个tile上运行5个逻辑核时,每个逻辑核的主频为600MHz/5=120MHz,同理可得运行8个逻辑核时,单个逻辑核的主频为75MHz。

如果一个逻辑核正在等待一个资源(见下文)满足指定的条件(例如定时器达到所需的值),那么它可能会被置于“暂停状态”。当一个逻辑核处于暂停状态时,它将从xcore硬件调度器调度的逻辑核列表中移除。一旦资源满足所需的条件,逻辑核就会被重新放回待调度的逻辑核列表中。

核心的运行速度可能因设备配置而异,但保证具有最低的MIPS。

在xC中,您可以通过与特定 tile 相关联的core数组来引用一个核心。例如,tile[0].core[1]tile[1].core[7]

定时器

每个 tile 都有一个以指定速率运行的参考时钟。在XMOS XS-1设备上,参考时钟频率为100MHz。与此时钟关联的是一个计数器。在XMOS XS-1设备上,该计数器宽度为32位。

提示

在XCORE-200和XCORE.AI架构的设备上,参考时钟频率仍为100MHz。

在xC中,使用timer类型来引用这个时钟。您可以声明这种类型的变量,并对这些变量进行操作,读取当前时间并等待特定的超时事件发生。

通信结构

在核心之间存在一个通信结构(即xCONNECT)。通信结构是通道端之间在网络中的物理连接,它允许任何一个通道端向任何其他通道端发送数据。

通信结构允许任何 tile 上的任何核心访问任何其他 tile 上的任何其他核心。这意味着任何软件任务都可以与另一个任务进行事务交换,无论另一个任务是否在同一个核心上(甚至不在同一个 tile 上也可以)。

链接是有方向性的;因此,如果通道端A向通道端B发送数据,然后(在链接关闭之前)BA发送数据回来,那么就会打开两个链接。这两个链接不一定会走相同的路由。在单个节点内部,通道端之间的通信容量总是至少足够打开两个链接。在节点之间,容量取决于连接的物理链接的数量。

I/O

每个 tile 都有自己的I/O子系统,由端口和时钟块组成。这些的详细信息在I/O中给出。在xC中,端口和时钟块分别由xC的语法类型portclock表示。