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

生成多个可控脉冲信号

本案例研究介绍了如何使用XMOS多核扩展(xC)在C语言中编写一个可控信号发生器的应用程序,即一个根据应用程序变化输出多个信号的应用程序:

image-20231212133555626

这种类型的应用程序类似于用于控制外部硬件(如步进电机)的应用程序。该应用程序由一个period_calculator任务组成,用于确定信号的周期。这将根据发生器的用途而变化。它通过一个缓冲任务连接到多个信号驱动任务。缓冲任务将周期的计算与驱动任务分离开来。

image-20231212133611986

获取周期

信号驱动任务需要知道要输出的周期。为了实现这一点,它从并行运行的另一个任务中获取周期。任务可以通过定义的接口相互通信。因此,在端口驱动任务和计算器之间,可以定义一个允许端口驱动程序请求计算器下一个周期的接口:

image-20231212133639334

接口period_if定义如下:

interface period_if {
int get_period();
};

信号驱动任务

信号驱动任务基于定时器事件重复唤醒。在每个事件中,它设置下一个端口输出,然后等待下一个事件。该任务的时间线如下所示:

image-20231212133659154

该代码的顶层循环如下所示:

while (1) {
select {
case tmr when timerafter(tmr_timeout):> void:
// [ 设置端口输出 ] ...
tmr_timeout += period;
break;
}
}

select语句等待事件,并表示“等待名为tmr的硬件定时器超过值tmr_timeout。当发生此事件时,代码设置端口输出并重置超时,以便等待下一个事件。任务在等待定时器事件时暂停,不执行任何操作。这段时间可以用于其他用途(在本例中是运行其他信号生成任务)。

信号驱动任务接受端口输出和接口连接作为参数,通过接口连接可以获取所需的周期。

[[combinable]]
void signal_driver(port p, client interface period_if updater)
{
// ... 任务初始化 ...
// 任务初始化其内部状态,然后进入一个while(1) select循环,使用定时器选择周期性更新。定时器事件通过select case在主循环中发生:

while (1) {
select {
case tmr when timerafter(tmr_timeout):> void:
// 通过接口连接获取当前周期
// 在这种情况下,首先要做的是通过接口连接获取包含周期值的缓冲区中的当前周期。
period = updater.get_period();

// 设置端口输出
next_port_time += period;
val = ~val;
p <: val @ next_port_time;

// 更新定时器输出,使其在下一个端口输出之后发生
tmr_timeout += period;
// 继续循环并在下一个定时器点处发生事件
}
}
}

image-20231212134045548

这将使p在端口时间next_port_time输出:

image-20231212134117640

在返回主循环之前,更新定时器输出,使其在下一个端口输出之后发生(请注意,定时器和端口运行在相同的基础时钟上):

tmr_timeout += period;

因此,现在循环将继续,并在下一个定时器点处发生事件:

image-20231212134237365

由于port_driver任务由一个形式为while(1) { select ... }的主循环组成,因此可以标记为可组合。这种类型的函数可以在同一个逻辑核上组合在一起。函数的顶层循环一起运行,因此在任何时候,核心都可以处理其中一个组合任务的事件。

将更新操作与驱动程序解耦

如果信号驱动程序直接与计算周期的应用程序相连,就会产生问题。驱动任务依赖周期计算器,因为它需要调用同步接口函数get_period。每次设置输出时,它都会暂停以等待周期计算器提供周期值。如果周期计算器还没有准备好,就会出现问题。为了解决这种时间依赖性,可以在驱动任务和周期计算器之间引入一个缓冲任务。

缓冲任务负责维护一个最新周期值的列表。它不仅可以更新端口驱动任务,还可以接收周期计算器的更新。值得注意的是,周期计算器与缓冲之间有了一种新的接口类型:

interface supply_period_if {
// 为信号驱动器``n``在缓冲区中设置``period``周期值
void set_period(int n, int period);
// 获取缓冲区中请求更新的周期索引。
[[ clears_notification ]] int get_next_required_period_index();
// 当信号生成器使用了缓冲区的前一个值时,会发出此通知。
[[ notification ]] slave void demand_next_period();
};

中间缓冲任务同时作为周期计算器和信号生成器的服务端。实现缓冲任务相对简单。

[[ distributable ]]
void buffer(server interface supply_period_if c_supplier, server interface period_if c_driver[n], unsigned n) {
int period[MAX_SIGNALS];
int next_index = 0;
for (int i = 0; i < n; i++)
period[i] = INITIAL_PERIOD;
while (1) {
select {
case c_driver[int i].get_period() -> int x:
x = period[i];
c_supplier.demand_next_period();
next_index = i;
break;
case c_supplier.get_next_required_period_index() -> int index:
index = next_index;
break;
case c_supplier.set_period(int i, int val):
period[i] = val;
break;
}
}
}

这个顶层循环基于接口调用进行选择操作。这里有几个语法点需要说明:

  • -> int x 表示变量x包含接口调用的返回值。
  • c_driver[int i] 遍历接口数组c_driver的所有元素(如果选择了某个元素,变量i将持有该元素的索引)。

注意,这个任务被标记为可分发。因为它只处理来自其他任务接口的请求,所以如果所有其他任务都在同一个处理器 Tile 上,那么缓冲任务就不会占用一个独立的逻辑核心,而是利用它所连接的客户端的核心。任务的资源在客户端之间共享,这样缓冲任务就成了任务之间的共享内存缓冲区。

顶层设计

现在,整个应用程序的顶层设计如下所示:

// 此函数负责计算周期并填充中间缓冲区。
[[ combinable ]] void calc_periods(client interface supply_period_if c);
port ps[4] = {XS1_PORT_1A, XS1_PORT_1B, XS1_PORT_1C, XS1_PORT_1D};
int main() {
interface period_if c_update[4];
interface supply_period_if c_supplier;
par {
on tile[0].core[0]: signal_driver(ps[0], c_update[0]);
on tile[0].core[0]: signal_driver(ps[1], c_update[1]);
on tile[0].core[0]: signal_driver(ps[2], c_update[2]);
on tile[0].core[0]: signal_driver(ps[3], c_update[3]);
on tile[0]: buffer(c_supplier, c_update, 4);
on tile[0]: calc_periods(c_supplier);
}
return 0;
}

主函数通过并行语句par来并行运行各个任务,并使用接口变量声明来连接各个任务。四个信号驱动任务在同一个逻辑核心上运行,而calc_periods任务在另一个(未命名的)逻辑核心上运行。由于缓冲任务是可分发的,它不占用自己的逻辑核心,这意味着整个应用程序只占用两个逻辑核心。

应用程序(计算周期长度)

calc_periods任务作为信号驱动程序的客户端,负责设置信号周期长度。这里的示例很简单,它为每个信号设置了一个固定的周期。它响应demand_next_period事件,当事件发生时,它会确定所需的周期是多少,并将该值设置到缓冲区中:

[[ combinable ]]
void calc_periods(client interface supply_period_if c);
{
while (1) {
select {
case c.demand_next_period():
int i = c.get_next_required_period_index();
// 为i计算周期
c.set_period(i, (INITIAL_PERIOD * (i + 1) * 2) / 3);
break;
}
}
}