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个端口。
图18:不同宽度的端口
| 端口宽度 | 端口数量 | 端口名称 |
|---|---|---|
| 1 | 16 | 1A, 1B, 1C, 1D, 1E, 1F, 1G, 1H, 1I, 1J, 1K, 1L, 1M, 1N, 1O, 1P |
| 4 | 6 | 4A, 4B, 4C, 4D, 4E, 4F |
| 8 | 4 | 8A, 8B, 8C, 8D |
| 16 | 2 | 16A, 16B |
| 32 | 1 | 32A |
在你的代码中,你可以通过声明端口类型的变量来访问端口。头文件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: 输出波形图 展示了此程序的输出结果。
图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:输入波形图展示了这个程序的示例输入和预期输出。
图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_out和p_clock_out。
声明语句
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呈现了端口计数器、时钟信号以及由端口驱动的数据的情况。

处理器的输出会使端口在其时钟的下一个下降沿驱动出数据;这些数据将被端口保存,直到执行下一个输出 操作为止。
使用外部时钟
下面的程序将一个端口配置为将数据的采样与外部时钟同步:
#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_in和p_clock_in。

语句
configure_clock_src(clk, p_clock_in);
配置了1位输入端口p_clock_in,使其为时钟clk提供边沿。每当该端口采样的值发生变化,就会生成一个边沿。
接着,
configure_in_port(p_in, clk);
语句将输入端口p_in设置为被clk时钟驱动。
图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展示了端口计数器、时钟信号以及由端口驱动的数据。
端口计数器在时钟的下降沿上递增。对于没有提供值的中间边沿,端口会继续使用其先前输出的数据驱动引脚。
使用缓冲端口
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_in、p_out和p_clock_in这些端口。
声明语句
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循环中执行各个语句的相对顺序波形。
前三个输入值分别为0x1、0x2和0x4,对应的输出值为0x2、0x3和0x5。
图28演示了硬件中缓冲操作的过程。它展示了处理器执行while循环以将数据输出到端口的过程。端口会对这些数据进行缓存,从而使得在端口驱动完一整个周期内先前输出的数据期间,处理器可以继续执行后续指令。每当时钟发生下降沿时,端口会从其缓冲区取出下一个字节的数据并驱动至其引脚上。只要循环中的指令执行时间短于端口的时钟周期,每个时钟周期都能在引脚上驱动一个新的值。
在上升沿之前就执行的第一个输入语句意味着并未使用到输入缓冲区。处理器总是在采样之前已经准备好接收下一个数据,这导致了处理器的阻塞,从而有效地使其运行速度降至与端口相同的频率。然而,如果第一个输入发生在采样到第一个值之后,此时输入缓冲区会保持数据直至处理器准备好接收它,且每个输出操作都会阻塞直到之前输出的值被驱动出去。
定时操作代表了未来某一时刻。波形和比较器逻辑使得定时输出可以被缓冲,然而对于定时输入和条件输入,执行输入操作之前,缓冲区将会被清空。