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

IO与时序

XMOS微控制器不仅允许用户编程实现实时并行的应用程序,还可执行复杂的I/O协议。这一过程通过在微控制器内部进行编程实现,在硬件响应端口利用C语言的多核扩展完成。

在XMOS设备上,我们采用引脚与外部组件对接。当前,每个XMOS设备都配有64个数字I/O引脚,这些引脚可以作为输入或输出使用。I/O引脚的运行电压通常为3.3V。但需要注意的是,并非所有产品的封装都能使外部访问所有64个引脚 - 请参阅产品Datasheet以确定可用的引脚数量。

在提到引脚时,我们遵循如下命名约定:XnDpq,其中n是设备内的 tile 编号,pq则是引脚的编号(例如,X0D05)。

提示

这里的命名通常与XMOS芯片的Datasheet相呼应,例如XU316-1024-QF60B-PP24中的第九页。

端口

设备上的引脚是通过硬件响应端口来访问的。这些端口负责操控引脚上的输出或者对输入数据进行采样。

各个端口的宽度不同:有1位、4位、8位、16位以及32位的端口。一个n位的端口可以同时驱动或采样n位的数据。

在目前的设备中,每块 tile 都配备了29个端口。

image-20230709100445907

图18:不同宽度的端口

端口宽度端口数量端口名称
1161A, 1B, 1C, 1D, 1E, 1F, 1G, 1H, 1I, 1J, 1K, 1L, 1M, 1N, 1O, 1P
464A, 4B, 4C, 4D, 4E, 4F
848A, 8B, 8C, 8D
16216A, 16B
32132A

在你的代码中,你可以通过声明端口类型的变量来访问端口。头文件xs1.h定义了初始化特定端口访问的宏,用以创建端口变量。例如,下面的声明将创建一个名为p的端口变量,用于访问4A端口:

#include <xs1.h>
port p = XS1_PORT_4A;

由于端口一次性输入和输出所有位,所以它们应当被应用于逻辑上需要同时工作的输入输出中。例如,一个4位端口并不是设计来驱动4个独立的信号的(如串行总线的时钟和数据线)- 对这种情况更适合使用独立的1位端口。然而,当用于从4位宽的数据总线输入或输出时,4位端口表现得非常高效。

外部引脚和端口之间有固定的映射关系。一些引脚映射到多个端口,并且通常不应同时使用重叠的端口。端口与引脚之间的映射可以在相关设备的数据手册中找到。

混合使用8bit port与1bit port

以XCORE.AI平台为例,在XU316-1024-QF60B-PP24 datasheet的第十页中,X0D36(PIN43)被同时定义成1M(1bit)与8D0(8bit)端口,在编写程序时,您不应将X0D36同时用作1bit与8bit端口。

然而,将这个8bit端口上的其他PIN作为8bit端口使用是非常常见的。您可以仅使用8bit端口上的剩余几位,在上面的例子中,8D4到8D7可以正常使用。

您可以像这样声明端口port p_8bit = XS1_PORT_8D,但仅使用端口的后4位,即8D4到8D7。这样做的好处是,您可以最大限度地使用额外的多bit引脚,而不影响其他引脚

时钟模块

所有端口都与时钟同步——它们连接到设备内的一个时钟块,控制来自端口的读写操作。时钟块向端口提供规律的时钟信号。

每个端口内部都有一个叫做移位寄存器(shift register)的组件,根据端口当前是输入模式还是输出模式,它会暂存要输出的数据或新输入的数据。在每一次时钟脉冲发生时,端口会将外部引脚的状态采样至移位寄存器,或者根据移位寄存器的内容操控外部引脚。因此,当程序“输入”或“输出”给一个端口时,实质上是在读取或写入移位寄存器。

每个 tile 都配备了六个时钟块。任何一个端口都可以连接到这六个时钟块中的任意一个。每个端口都可以被设置为以下两种模式之一:

模式描述
除法模式时钟运行的频率是芯片核心时钟频率的整数倍(例如,对于500MHz的芯片,此模式下时钟运行在500MHz)。
外部驱动模式时钟由端口输入控制。

第二种模式主要用于将I/O与外部时钟同步。例如,如果设备通过MII协议连接到以太网PHY,一个时钟块就可以连接到与RXCLK信号相连的端口,然后这个时钟块就可以驱动采样RXD信号数据的端口。

默认情况下,所有端口都连接到被指定为参考时钟块的0号时钟块,它始终运行在100MHz的频率下。

你可以通过声明一个类型为clock的变量来访问其它的时钟。在xs1.h头文件中声明了这个类型和代表设备上的时钟的初始化器。例如,以下代码就声明了一个变量,允许你访问2号时钟块:

#include <xs1.h>
clock clk = XS1_CLKBLK_2;

您可以使用在xs1.h中定义的配置库函数将端口和时钟模块连接在一起。这些函数的详细信息在以下章节中说明。

输出数据

下面是一个简单的程序,用于将引脚高电平和低电平切换:

#include <xs1.h>
out port p = XS1_PORT_1A;

int main(void) {
p <: 1;
p <: 0;
}

以下代码:

out port p = XS1_PORT_1A;

声明了一个名为p的输出端口,它指向被标识为1A的1位端口。

以下命令:

p <: 1;

将数值1输出到端口p,使得该端口驱动其对应的引脚转为高电平。这个端口将一直维持其引脚在高电平状态,直到下一条语句执行:

p <: 0;

这条语句将数值0输出到端口,使得该端口驱动其对应的引脚转为低电平。图19: 输出波形图 展示了此程序的输出结果。

image-20230709105352374

图19:输出波形图

引脚在初始状态下是没有被驱动的;执行第一条输出指令后,它被驱动为高电平;执行第二条输出指令后,它被驱动为低电平。总的来说,当向一个n位端口输出时,最低有效的n位将被输出到引脚上,其余的则会被忽略。

所有的端口都必须声明为全局变量,并且不可以使用相同的端口标识符来初始化两个端口。一旦初始化完成,端口就不能再被重新赋值。尽管可以将端口作为参数传递给函数,但要确保端口并未出现在函数的多个参数中,否则将会产生非法别名。

输入数据

以下程序将持续对输入端口的4个引脚进行采样,并且在采样值超过9时将输出端口置为高电平:

#include <xs1.h>
in port p_in = XS1_PORT_4A;
out port p_out = XS1_PORT_1A;

int main(void) {
int x;
while (1) {
p_in :> x;
if (x > 9)
p_out <: 1;
else
p_out <: 0;
}
}

以下代码:

in port p_in = XS1_PORT_4A;

创建了一个名为p_in的输入端口,它指向被标识为4A的4位端口。

语句

p_in :> x;

将端口p_in采样到的值输入到变量x中。图20:输入波形图展示了这个程序的示例输入和预期输出。

image-20230709110612412

图20: 输入波形图

此程序会持续从p_in端口读取数据:当采样到0x8时,输出被驱动为低电平;当采样到0xA时,输出被驱动为高电平;当采样到0x2时,输出再次被驱动为低电平。每个输入值可能会被多次采样。

通过输入端口检测触发事件

一个端口可以在引脚达到以下两种状态之一时触发事件:等于或不等于某个值。以下程序使用select语句来计数引脚上的转变次数,直到它达到某个指定的值:

#include <xs1.h>

void wait_for_transitions(in port p, unsigned n) {
unsigned i = 0;
p :> x;
while (i < n) {
select {
case p when pinsneq(x):> x:
i++;
break;
}
}
}

以下语句:

p when pinsneq(x):> x;

指示端口p在其引脚的值不等于x时才进行采样,并向任务提供可响应的事件。当满足这个条件时,当前的值将会被存储回x

再举一个例子,一个任务可以在4位端口上等待以太网前导码的出现,其条件如下:

p_eth_data when pinseq(0xD):> void:

在这里,:>后使用void表示输入值并没有被存放到任何地方。

相较于在软件中轮询端口,使用基于输入端口的事件检测的更为节省功耗。因为这使得处理器可以处于空闲状态,从而减少功耗,而端口则保持活跃,持续监测其引脚。

生成时钟信号

下面的程序将一个1位端口配置为以12.5MHz的频率进行时钟驱动,同时输出对应的时钟信号,并通过一个8位端口其输出数据:

#include <xs1.h>

out port p_out = XS1_PORT_8A;
out port p_clock_out = XS1_PORT_1A;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_rate(clk, 100, 8);
configure_out_port(p_out, clk, 0);
configure_port_clock_output(p_clock_out, clk);
start_clock(clk);
for (int i = 0; i < 5; i++)
p_out <: i;
}

该程序根据图21中所示的方式配置了端口p_outp_clock_out

image-20230709111853019
图21:端口配置图

声明语句

clock clk = XS1_CLKBLK_1;

定义了一个被命名为clk的时钟,其对应时钟块标识符为XS1_CLKBLK_1。全局变量形式的时钟在声明时需用到一个唯一的资源标识符进行初始化。

接着的语句,

configure_clock_rate(clk, 100, 8);

clk这一时钟的频率配置为12.5MHz。因为xC只支持整数算术类型,所以频率采取分数(100/8)方式来设定。

此外,

configure_out_port(p_out, clk, 0);

的作用是配置输出端口p_out,使其由clk时钟驱动,并在初始阶段在其引脚上产生值为0的信号。

而执行

configure_port_clock_output(p_clock_out, clk)

则会令clk时钟信号传输至与p_clock_out端口相连的引脚,允许接收方根据此信号来采样p_out端口产生的数据。

执行

start_clock(clk);

可以使时钟块开始产生边缘触发。

每个端口内部都配有一个16位计数器,该计数器会在每个时钟下降沿时增加。图22呈现了端口计数器、时钟信号以及由端口驱动的数据的情况。

image-20230709113000341
图22:波形图

处理器的输出会使端口在其时钟的下一个下降沿驱动出数据;这些数据将被端口保存,直到执行下一个输出操作为止。

使用外部时钟

下面的程序将一个端口配置为将数据的采样与外部时钟同步:

#include <xs1.h>

in port p_in = XS1_PORT_8A;
in port p_clock_in = XS1_PORT_1A;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_src(clk, p_clock_in);
configure_in_port(p_in, clk);
start_clock(clk);
for (int i = 0; i < 5; i++)
p_in :> int x;
}

该程序根据图23所示的方式配置了端口p_inp_clock_in

image-20230709122550187
图23:端口配置图

语句

configure_clock_src(clk, p_clock_in);

配置了1位输入端口p_clock_in,使其为时钟clk提供边沿。每当该端口采样的值发生变化,就会生成一个边沿。

接着,

configure_in_port(p_in, clk);

语句将输入端口p_in设置为被clk时钟驱动。

图24展示了端口计数器、时钟信号以及实例输入激励的情况。

image-20230709122701620

图24:波形图

处理器的输入会导致端口在下一个上升沿上对数据进行采样。输入的值分别为0x7、0x5、0x3、0x1和0x0。

在特定时钟沿上进行输入/输出操作

通常需要在端口的时钟相对于特定时间执行输入/输出操作。下面的程序在第三个时钟周期将引脚置高,第五个时钟周期将引脚置低:

void do_toggle(out port p) {
int count;
p <: 0 @ count; // 带有时间戳的输出
while (1) {
count += 3;
p @ count <: 1; // 定时输出
count += 2;
p @ count <: 0; // 定时输出
}
}

语句

p <: 0 @ count;

执行一个带时间戳的输出操作,向端口p输出值0,并读取此时端口计数器的值到变量count中。这个计数器表示了数据在引脚上被驱动的时间点。然后,程序对count增加3,并执行定时输出语句:

p @ count <: 1;

此语句使得端口等待,直到其计数器的值等于count + 3(相当于推进了三个时钟周期);这时,端口将其引脚电平提升为高。最后两条语句让下一次输出延迟两个时钟周期。图25展示了端口计数器、时钟信号以及由端口驱动的数据。

端口计数器在时钟的下降沿上递增。对于没有提供值的中间边沿,端口会继续使用其先前输出的数据驱动引脚。

image-20230709122333345
图25:波形图

使用缓冲端口

XMOS设备提供了缓冲区,用以优化在时钟端口上执行I/O操作的程序性能。缓冲器可以暂存处理器输出的数据,直至端口时钟的下一个下降沿,期间允许处理器执行其他指令。同时,它也能储存由端口采样的数据,直到处理器准备好接收。借助缓冲器,单一线程能够并行地在多个端口上进行I/O操作。

以下程序利用缓冲端口将端口上数据的采样和驱动与计算过程解耦:

#include <xs1.h>

in buffered port:8 p_in = XS1_PORT_8A;
out buffered port:8 p_out = XS1_PORT_8B;
in port p_clock_in = XS1_PORT_1A;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_src(clk, p_clock_in);
configure_in_port(p_in, clk);
configure_out_port(p_out, clk, 0);
start_clock(clk);

while (1) {
int x;
p_in :> x;
p_out <: x + 1;
f();
}
}

此程序按照图26所展示的方式配置了p_inp_outp_clock_in这些端口。

image-20230709123208525
图26:端口配置图

声明语句

in buffered port:8 p_in = XS1_PORT_8A;

定义了一个名为p_in的缓冲输入端口,其对应的是8位端口标识符8A。

语句

configure_clock_src(clk, p_clock_in);

配置了一个1位输入端口p_clock_in,使其为时钟clk提供边沿。

接着:

configure_in_port(p_in, clk);

将输入端口p_in设置为由clk时钟驱动。

然后:

configure_out_port(p_out, clk, 0);

此语句将输出端口p_out设置为由clk时钟驱动,并在初始阶段在其引脚上产生值为0的信号。

图27展示了该程序的示例输入激励以及预期输出。它还描绘了处理器在while循环中执行各个语句的相对顺序波形。

image-20230709123237885
图27:波形图(相对于处理器执行)

前三个输入值分别为0x1、0x2和0x4,对应的输出值为0x2、0x3和0x5。

图28演示了硬件中缓冲操作的过程。它展示了处理器执行while循环以将数据输出到端口的过程。端口会对这些数据进行缓存,从而使得在端口驱动完一整个周期内先前输出的数据期间,处理器可以继续执行后续指令。每当时钟发生下降沿时,端口会从其缓冲区取出下一个字节的数据并驱动至其引脚上。只要循环中的指令执行时间短于端口的时钟周期,每个时钟周期都能在引脚上驱动一个新的值。

image-20230709123338871
图28:端口的硬件逻辑

在上升沿之前就执行的第一个输入语句意味着并未使用到输入缓冲区。处理器总是在采样之前已经准备好接收下一个数据,这导致了处理器的阻塞,从而有效地使其运行速度降至与端口相同的频率。然而,如果第一个输入发生在采样到第一个值之后,此时输入缓冲区会保持数据直至处理器准备好接收它,且每个输出操作都会阻塞直到之前输出的值被驱动出去。

注意

定时操作代表了未来某一时刻。波形和比较器逻辑使得定时输出可以被缓冲,然而对于定时输入和条件输入,执行输入操作之前,缓冲区将会被清空。

同步多个端口上的时钟输入/输出

通过配置多个缓冲端口,使它们从同一源接收时钟信号,单个线程就可以并行地在这些端口上进行数据采样和驱动。

下述程序首先将自身与一个时钟周期的开始进行同步,确保在下一个下降沿前有最大量的时间,然后将一个8位字符值序列并行地输出到两个4位端口。

#include <xs1.h>

out buffered port p :4 = XS1_PORT_4A;
out buffered port q :4 = XS1_PORT_4B;
in port p_clock_in = XS1_PORT_1A;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_src(clk, p_clock_in);
configure_out_port(p, clk, 0);
configure_out_port(q, clk, 0);
start_clock(clk);

p <: 0; // 开始输出
sync(p); // 同步到下降沿

for (char c = 'A'; c <= 'Z'; c++) {
p <: (c & 0xF0) >> 4;
q <: (c & 0x0F);
}
}

语句

sync(p);

使处理器等待到下一个下降沿,在此下降沿上,缓冲区内最后的数据已经在一个完整周期内被驱动,从而确保下一条指令紧跟在下降沿后执行。这样可以保证循环中随后的两个输出语句在同一个时钟周期内执行。图29显示了由处理器输出并且被两个端口驱动的数据。

提示

建议将同步操作定位到上升沿的方式是:使用标准库函数clearbuf来清空缓冲区,然后执行输入操作。

image-20230709124515987
图29:处理器将数据同步到两个输出端口

使用端口进行输出数据序列化

XMOS设备提供硬件支持以应对通信协议中常见的操作。一个端口可以被配置为执行序列化操作,这在需要通过只有几位宽度的端口传输数据时尤其有用,并且支持握手信号(strobing )操作,对于伴随着独立数据有效信号的数据传输也很有用。把这些任务交给端口处理可以让更多的处理器时间用于执行计算。

一个受时钟控制的端口可以将数据序列化,从而降低执行输出所需要的指令数量。以下程序将一个32位值输出到8个引脚,利用时钟来确定每个8位值驱动的时长。

#include <xs1.h>

out buffered port :32 p_out = XS1_PORT_8A;
in port p_clock_in = XS1_PORT_1A;
clock clk = XS1_CLKBLK_1;

int main(void) {
int x = 0xAA00FFFF;

configure_clock_src(clk, p_clock_in);
configure_out_port(p_out, clk, 0);
start_clock(clk);

while (1) {
p_out <: x;
x = f(x);
}
}

关于语句

out buffered port:32 p_out = XS1_PORT_8A;

它定义了一个名为p_out的端口,使其能从一个32位移位寄存器驱动8个引脚。其中,类型port:32表示每次输出操作所传输的位数(即传输宽度)。初始化设定XS1_PORT_8A则确定了与该端口连接的物理引脚数目(也就是端口宽度)。图30展示了该程序产生并通过此端口驱动的数据。

image-20230709124701890
图30:序列化输出波形图

通过将序列化操作卸载给端口,处理器只需要在每4个时钟周期输出一次。在时钟的每个下降沿上,移位寄存器的最低有效8位被驱动到引脚上;然后,移位寄存器向右移动8位。

提示

进行序列化的端口必须使用关键字buffered进行限定;详细说明请参见XM000971-PC文档。

使用端口进行输入数据的反序列化

端口能够执行数据反序列化,这样就可以降低输入数据所需的指令数量。以下程序在一个输入端口上实施4位至8位的转换,整个过程由一个25MHz的时钟进行控制。

#include <xs1.h>

in buffered port:8 p_in = XS1_PORT_4A;
out port p_clock_out = XS1_PORT_1A;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_rate(clk, 100, 4);
configure_in_port(p_in, clk);
configure_port_clock_output(p_clock_out, clk);
start_clock(clk);

while (1) {
int x;
p_in :> x;
f(x);
}
}

该程序将p_in声明为一个宽度为4位、传输宽度为8位的端口,意味着在处理器需要输入之前,该端口能够预先采样两个4位值。如同输出操作一样,通过反序列化器,我们可以减少获取数据所需要的指令数量。图31展示了输入刺激示例以及数据在端口缓冲区中可供输入的时间段。

image-20230709130057664
图31:反序列化的输入波形图

数据会在时钟上升沿处被采样,在进行移位操作时,最低有效的四位会被首先读取。这些已采样的数据将在端口的缓冲区内保留两个时钟周期,以便进行输入操作。最初输入的两个值是0x28和0x7A。

输入伴随着有效数据信号

时钟驱动端口能够识别一种名为ready-in strobe的信号,该信号用于判断伴随数据是否有效。在以下程序中,只有当ready-in信号为高电平状态时,才从时钟驱动端口接收数据。

#include <xs1.h>

in buffered port:8 p_in = XS1_PORT_4A;
in port p_ready_in = XS1_PORT_1A;
in port p_clock_in = XS1_PORT_1B;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_src(clk, p_clock_in);
configure_in_port_strobed_slave(p_in, p_ready_in, clk);
start_clock(clk);

p_in :> void;
}

关于语句

configure_in_port_strobed_slave(p_in, p_ready_in, clk);

它将输入端口p_in配置为:仅当在端口p_ready_in上采样到的值为1时,才进行数据采样。ready-in端口的宽度必须为1位。图32展示了示例输入刺激以及此程序接收的数据。

image-20230709131145753
图32:带有数据有效信号的输入数据

只要ready-in信号处于高电平状态,数据就会在时钟的上升沿被采样。端口会采样两个4位的数值,并将这两个数值组合生成一个8位的数值,以供处理器输入;此次输入的数据是0x28。这些端口具有单一入口缓冲区,这意味着,在ready-in信号在接下来的两个时钟上升沿期间保持高电平之前,数据始终都可供输入。请注意,无论strobe信号是否为高电平,每个时钟周期都会使端口计数器增加。

输出数据和有效数据信号

时钟驱动的端口可以在输出数据时生成一个 ready-out strobe 信号。下面的程序使一个输出端口在驱动4位端口上的数据时产生一个数据有效信号。

#include <xs1.h>

out buffered port:8 p_out = XS1_PORT_4B;
out port p_ready_out = XS1_PORT_1A;
in port p_clock_in = XS1_PORT_1B;
clock clk = XS1_CLKBLK_1;

int main(void) {
configure_clock_src(clk, p_clock_in);
configure_out_port_strobed_master(p_out, p_ready_out, clk, 0);
start_clock(clk);

p_out <: 0x85;
}

关于语句

configure_out_port_strobed_master(p_out, p_ready_out, clk, 0);

它将输出端口p_out配置为:只要有数据输出,就使端口p_ready_out为高电平。ready-out端口的宽度必须为1位。图33展示了由此程序驱动的数据和脉冲(strobe)信号。

image-20230709131457493
图33:带有数据有效信号的输出数据

端口在两个时钟周期内驱动两个4位值,此期间 ready-out 信号保持高电平。还可以实现使用 ready-in strobe 信号输出数据和使用 ready-out strobe 信号输入数据的控制流算法;当两个信号都配置时,端口实现了一种对称脉冲(strobe)协议,该协议使用时钟进行数据通信的握手(参见 XM-000969-PC)。

用于 strobing 的端口必须使用关键字 buffered 进行限定;详细说明请参见 XM000971-PC 文档。