文章目录
本篇目的
继续XMOS的程序开发-xc语言的任务间的通讯机制:使用接口的方式,以及xC的事件处理。
开发环境
- 硬件平台使用官方评估板"xCORE VOCAL FUSION XP-VF3100-BASE"
- IDE开发环境win10 下的 xTIMEcomposer
简介
C语言可以处理这样的事件:按键按下,定时器溢出,串口接收到数据。C语言一般通过中断/异常处理这样的事件。xC则是通过select的事件处理模式来处理这样的事件。
接口则是用于一个任务向另一个任务传递事务的方式,使用接口通讯的这两个任务,一个叫接口客户端,一个叫做接口服务端。比如接口客户端可以发起一个事务:键值为1的按键按下了。服务端通过接口可以获取到这个事务,并通过select事件处理方式对这个事务执行处理。
通过接口传递的事务是有类型定义的,这需要从接口说起。
接口定义
接口使用关键字interface定义,定义如下:
interface T {
transaction1(...);
transaction2(...);
...
};
interface是关键字,T是接口类型名称,interface体内的transaction1和transaction2等表示接口传递的事务,事务按照C函数那样定义,即函数名称和函数参数。比如:
interface my_interface {
float add(float a, float b); // 加法事务
void inc(int& a); // 自加1事务
};
定义了接口my_interface,事务可以传c语言支持的参数,可以传引用参数,可以带返回值,因此同一个接口可以用于任务之间的双向传递。
类似C语言定义了结构一样,还需要定义结构的实例才能使用;xC的接口也一样,使用时要定义接口实例,比如:
interface my_interface mi; // 定义接口实例mi
select事件处理
语法如下:
select {
case event1: // 有event1事件
// 处理event1事件
break;
case event2: // 有event2事件
// 处理event2事件
break;
...
}
有点类似于C语言的没有default分支的switch case语句,但是只是写法有点像而已,实际上完全不一样。select处理事件,是阻塞等待事件发生的,如果没有任何一个case的事件发生的话,是阻塞不往下执行的。
event的写法和写接口实例的函数原型是一样的,比如接口interface my_interface定义了一个实例mi,那么
select {
case mi.add(float a, float b) -> float ret:
// 处理add事件,也需要给返回值ret赋值
ret = 2; // 给返回值赋值
break;
case mi.inc(int& a):
// 处理inc事件
break;
}
注意语法case mi.inc(int& a):
,mi.inc(int& a)
即实例.函数(参数列表)
这样的格式,是没有返回值的事件处理的写法。
case mi.add(float a, float b) -> float ret:
这个是带有返回值的写法,即实例.函数(参数列表) -> 返回值类型 返回值变量
。处理语句中给返回值变量赋值。
接口用于任务间通讯
上面简单的说明了接口和select事件处理之后,可以开始说明接口在任务间通讯的用法了。
前面简介中说:“接口则是用于一个任务向另一个任务传递事务的方式,使用接口通讯的这两个任务,一个叫接口客户端,一个叫做接口服务端。”
就是说接口用于任务间通讯包含3部分:
- 定义的接口
- 接口客户端
- 接口服务端
下面以一个例子说明,还是用上面定义的my_interface接口,说明接口如何在两个任务间双向通讯,如何通过接口传递事务,select事件处理机制如何处理事务。
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
float add(float a, float b); // 加法事务
void inc(int& a); // 自加1事务
};
/* 接口客户端 */
void task1(client interface my_interface i) {
printf("task1: transaction add\n");
float sum = i.add(1.2, 3.4);
printf("task1: sum=%f\n", sum);
int x = 5;
printf("task1: transaction inc\n");
i.inc(x);
printf("task1: x=%d\n", x);
}
/* 接口服务端 */
void task2(server interface my_interface i) {
while(1) {
select {
case i.add(float a, float b) -> float ret:
printf("task2: handle event: add(%f, %f)\n", a, b);
ret = a + b;
break;
case i.inc(int& a):
printf("task2: handle event: inc(%d)\n", a);
a++;
break;
}
}
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
par {
task1(i);
task2(i);
}
return 0;
}
执行结果
task1: transaction add
task2: handle event: add(1.200000, 3.400000)
task1: sum=4.600000
task1: transaction inc
task2: handle event: inc(5)
task1: x=6
首先看3个部分,定义了接口my_interface,之后interface my_interface就是一个接口类型,main函数给这个接口类型定义了一个实例 i,并把接口实例传递给并发的两个任务task1和task2,task1传递参数类型写成client interface my_interface,client表明是当客户端,task2传递参数写成server interface my_interface,server表明是服务端。
客户端发起事务传输,方式就跟调用c语言de 结构函数一样,i.add(1.2, 3.4)
和i.inc(x)
都是发起事务,客户端发起事务,服务端就能接收到事务。
服务端处理接收到的事务,通过select事件处理机制处理。当客户端发出add事务之后,服务端收到这个事务,会执行对应的case的代码,这个语法i.add(float a, float b) -> float ret
就是事件处理结果返回值传回去的方法,表示传回给ret这个float类型,case语句给ret赋值就可以了。select case语法中没有返回值的事件,则不需要写->的部分
。
从执行结果看正确的处理了1.2+3.4的事务,以及5自加1的事务。
接口用于任务间通讯的程序语法要求
不遵循要求就会产生编译时错误。
要求:
-
传递给并行任务的接口必须有且只有一个客户端和一个服务端
客户端必须在参数列表中用
client interface
表明;服务端必须在参数列表中用server interface
表明。 -
普通的事务只能从客户端发起(notification机制可以由服务端发起,后续再说)
-
select中的case要包含所有的事务处理
下面举几个错误的例子,假设上面的例子编程错误
- task1漏写了client修饰符
void task1(interface my_interface i)
编译错误
…/src/mytest.xc:86:23: error: interface parameter must have
server' or
client’ modifier
没有client修饰符,会认为没有客户端。
- task2没写server修饰符,即
void task2(interface my_interface i)
编译错误和没有client修饰符一样。没有server认为是没有服务端。
- 客户端多于一个或者服务端多于一个
增加一个task3做客户端或者服务端,编译时错误:
error: interface used in two tasks as client
或者
error: interface used in two tasks as server
- task2服务端发起事务
即在task2中调用float sum = i.add(1.2, 3.4);
发起事务,比如
void task2(server interface my_interface i) {
float sum = i.add(1.2, 3.4); // server端发起事务,错误
编译错误
error: trying to call interface function from a server interface
- task2的select case中少处理一项事务,即
void task2(server interface my_interface i) {
while(1) {
select {
case i.add(float a, float b) -> float ret:
printf("task2: handle event: add(%f, %f)\n", a, b);
ret = a + b;
break;
}
}
}
编译错误
error: missing case for interface function
inc' of interface
my_interface’
只要写了select case处理了某个接口事件,就要把改接口要处理的所有事件case语句写全。要不就不要用select case处理该。比如删除task2中的select case块,编译是不会错误的。
void task2(server interface my_interface i) {
while(1) {
}
}
// 这样修改不会产生编译时错误
- main函数只把接口实例传递给一个任务
比如以下的代码,main函数只把接口实例传递给task1或者task2,虽然不会产生编译时错误,只会警告,但是这样使用是没有任何意义的。
int main () {
interface my_interface i; // 定义一个接口实例,只传给一个任务
par {
task2(i);
}
return 0;
}
或者
int main () {
interface my_interface i; // 定义一个接口实例,只传给一个任务
par {
task1(i);
}
return 0;
}
编译结果都是警告
warning: `i’ not used in two parallel statements
但是这样的用法没什么意义。
任务间多个多个接口通讯
用于任务间通讯的接口,对于某单个的接口,遵循上面的原则就没问题;多个接口也是由多个遵循这些原则的接口组成的,任务可以是仅仅是某个接口的客户端,或者仅仅是某个接口的服务端,也可以是某个的接口的客户端,同时做另一个接口的服务端,只要遵循对于某个接口,有且只有一个客户端,有且只有一个服务端就可以了。
为了说明任务间使用多个接口通讯的工作方式,把例子简化点,使用两个接口,任务不对接口做任何操作,不发起事务,也不处理事务,便于理解多个接口做任务间通讯的工作方式。
- 例子1
如下代码:
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
float add(float a, float b); // 加法事务
void inc(int& a); // 自加1事务
};
/* 定义接口 */
interface you_interface {
float mul(float a, float b); // 乘法事务
void dec(int& a); // 自减1事务
};
/* 接口客户端 */
void task1(client interface my_interface i, client interface you_interface yi) {
}
/* 接口服务端 */
void task2(server interface my_interface i, server interface you_interface yi) {
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
interface you_interface yi; // 定义一个接口实例,传给客户端和服务端
par {
task1(i, yi);
task2(i, yi);
}
return 0;
}
在前一个代码的基础上多定义一个you_interface接口,task1和task2任务都是使用两个接口实例,task1做两个接口的客户端,task2做两个实例的服务端。
接口i有且只有一个客户端task1,有且只有一个服务端task2;接口yi有且只有一个客户端task1,有且只有一个服务端task2。
- 例子2
如下代码:
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
float add(float a, float b); // 加法事务
void inc(int& a); // 自加1事务
};
/* 定义接口 */
interface you_interface {
float mul(float a, float b); // 乘法事务
void dec(int& a); // 自减1事务
};
/* 接口客户端 */
void task1(client interface my_interface i, server interface you_interface yi) {
}
/* 接口服务端 */
void task2(server interface my_interface i, client interface you_interface yi) {
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
interface you_interface yi; // 定义一个接口实例,传给客户端和服务端
par {
task1(i, yi);
task2(i, yi);
}
return 0;
}
task1和task2任务都是使用两个接口实例,task1做i的客户端,yi的服务端,task2做i的服务端,yi的客户端。
接口i有且只有一个客户端task1,有且只有一个服务端task2;接口yi有且只有一个客户端task2,有且只有一个服务端task1。
- 例子3
如下代码:
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
float add(float a, float b); // 加法事务
void inc(int& a); // 自加1事务
};
/* 定义接口 */
interface you_interface {
float mul(float a, float b); // 乘法事务
void dec(int& a); // 自减1事务
};
/* 接口客户端 */
void task1(client interface my_interface i) {
}
void task3(client interface you_interface yi) {
}
/* 接口服务端 */
void task2(server interface my_interface i, server interface you_interface yi) {
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
interface you_interface yi; // 定义一个接口实例,传给客户端和服务端
par {
task1(i);
task3(yi);
task2(i, yi);
}
return 0;
}
task1做i的客户端,task3做yi的客户端,task2做i和yi的服务端。task1和task2通过接口i通讯,task3和task2通过yi通讯。
接口i有且只有一个客户端task1,有且只有一个服务端task2;接口yi有且只有一个客户端task3,有且只有一个服务端task2。
通过这几个例子应该能理解多个通道用于任务间通讯的用法了。
然后实际使用的时候,需要注意,任务发出事务后,会阻塞等到另一个任务处理事务之后才能往下执行,使用select case来处理事务的任务也会阻塞等待直到事务到来才往下执行,一不小心就会造成相互阻塞。
最后举一个2个接口用于任务间通讯的完整例子,task1做接口i的客户端,task3做yi的客户端,task2做2个接口的服务端,这样用,不会造成相互阻塞。
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
float add(float a, float b); // 加法事务
void inc(int& a); // 自加1事务
};
/* 定义接口 */
interface you_interface {
float mul(float a, float b); // 乘法事务
void dec(int& a); // 自减1事务
};
/* 接口客户端 */
void task1(client interface my_interface i) {
printf("task1: transaction add\n");
float sum = i.add(1.2, 3.4);
printf("task1: sum=%f\n", sum);
int x = 5;
printf("task1: transaction inc\n");
i.inc(x);
printf("task1: x=%d\n", x);
}
void task3(client interface you_interface yi) {
printf("task3: transaction mul\n");
float mul = yi.mul(1.2, 3.4);
printf("task3: mul=%f\n", mul);
int x = 5;
printf("task3: transaction dec\n");
yi.dec(x);
printf("task3: x=%d\n", x);
}
/* 接口服务端 */
void task2(server interface my_interface i, server interface you_interface yi) {
while(1) {
select {
case i.add(float a, float b) -> float ret:
printf("task2: handle event: add(%f, %f)\n", a, b);
ret = a + b;
break;
case i.inc(int& a):
printf("task2: handle event: inc(%d)\n", a);
a++;
break;
case yi.mul(float a, float b) -> float ret:
printf("task2: handle event: mul(%f, %f)\n", a, b);
ret = a * b;
break;
case yi.dec(int& a):
printf("task2: handle event: dec(%d)\n", a);
a--;
break;
}
}
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
interface you_interface yi; // 定义一个接口实例,传给客户端和服务端
par {
task1(i);
task3(yi);
task2(i, yi);
}
return 0;
}
运行结果正确
task3: transaction mul
task1: transaction add
task2: handle event: mul(1.200000, 3.400000)
task3: mul=4.080000
task2: handle event: add(1.200000, 3.400000)
task3: transaction dec
task1: sum=4.600000
task2: handle event: dec(5)
task1: transaction inc
task3: x=4
task2: handle event: inc(5)
task1: x=6
服务端发起事务通知(Notifications)
以上的方式,都是客户端发起事务,按以上的描述,如果服务端发起事务,会引起编译时错误。再者,客户端发起事务后,如果没有服务端处理事务,客户端会引起阻塞。
这节就说由服务端发起事务的方法:Notifications方法。
接口定义中定义一种事务 - notification事务,notification事务的特点:
- 由
[[notification]] slave
指定是notification事务 - 不能带参数和返回值
- 由服务端发起
- 不阻塞
- 接口定义
[[clears_notification]]
的函数用于客户端清除通知,同时发起事务 - 客户端没清除通知前,重复发起无效,客户端清除通知之后,可再次发起通知事务
举个例子:客户端要从硬件输入读取两个数据输入,用于加法计算,硬件输入准备好之后,服务端发起通知,客户端清除这个通知同时发起读取事务,服务端处理读取事务,并把读取的结果传给客户端,如此就完成了一次读取。读取了2次数据后,客户端发起加法的事务,服务端处理传回结果。
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
int add(int a, int b); // 加法事务
[[clears_notification]] int read_input();
[[notification]] slave void input_ready(); // 无参数,没返回值
};
/* 模拟从硬件读取到数据回来 */
int read_dat_from_hardware() {
const int s_hwdat[] = {12, 34, 56, 78};
static int hwdat_index = 0;
int dat = s_hwdat[hwdat_index];
hwdat_index = (hwdat_index + 1) % (sizeof(s_hwdat) / sizeof(s_hwdat[0]));
return dat;
}
/* 接口客户端 */
void task1(client interface my_interface i) {
int a[2];
int recv_count = 0;
while(recv_count < 2) {
select {
case i.input_ready():
printf("task1: 清除通知同时发起读输入的事务\n");
a[recv_count] = i.read_input(); // 清除 notification ,并引发服务端处理 read_input 事务
printf("task1: 读到输入数据为%d\n", a[recv_count]);
recv_count++;
break;
}
}
printf("task1: transaction add\n");
int sum = i.add(a[0], a[1]);
printf("task1: sum=%d\n", sum);
}
/* 接口服务端 */
void task2(server interface my_interface i) {
while(1) {
printf("task2: 通知客户端输入已准备\n");
i.input_ready();
select {
case i.add(int a, int b) -> int ret:
printf("task2: handle event: add(%d, %d)\n", a, b);
ret = a + b;
break;
case i.read_input() -> int dat:
printf("task2: 处理读输入事务,从硬件读输入并传回给客户端\n");
dat = read_dat_from_hardware();
break;
}
}
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
par {
task1(i);
task2(i);
}
return 0;
}
执行结果为
task2: 通知客户端输入已准备
task1: 清除通知同时发起读输入的事务
task2: 处理读输入事务,从硬件读输入并传回给客户端
task1: 读到输入数据为12
task2: 通知客户端输入已准备
task1: 清除通知同时发起读输入的事务
task2: 处理读输入事务,从硬件读输入并传回给客户端
task1: 读到输入数据为34
task2: 通知客户端输入已准备
task1: transaction add
task2: handle event: add(12, 34)
task1: sum=46
task2: 通知客户端输入已准备
用时序图表示更清除
接口数组
任务可以使用接口数组连接多个任务。
服务端使用interface_array[int index].接口事务原型
这样的方式处理接口数组的事务,这样客户端发起事务的时候,下标会传给index,接口事务原型的参数传输和前面所述的事务传输一样。
举例
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
void f(int x);
};
/* 接口服务端 */
void task1(server interface my_interface a[n], unsigned int n) {
while(1) {
select {
case a[int i].f(int x): // 接口数组处理事务的语法
printf("task1: 处理从接口%d发起的事务,值是%d\n", i, x);
break;
}
}
}
/* 接口客户端 */
void task2(client interface my_interface i) {
i.f(12);
}
/* 接口客户端 */
void task3(client interface my_interface i) {
i.f(34);
}
int main () {
interface my_interface a[2]; // 定义一个接口实例数组,传给客户端和服务端
par {
task1(a, 2);
task2(a[0]);
task3(a[1]);
}
return 0;
}
执行结果
task1: 处理从接口0发起的事务,值是12
task1: 处理从接口1发起的事务,值是34
扩展接口的客户端API
接口定义后,可以扩展定义客户端可以使用的API,即是说,可以扩展客户端可以发起的事务。比如原来定义了接口
interface 接口名 {
// 事务函数
};
要扩展一个事务,使用如下语法:
extends client interface 接口名: {
扩展的事务函数定义(client interface 接口名 实例, 参数列表) {
// 定义好函数功能
}
};
扩展客户端API有以下特点:
- 第一个参数必须是客户端接口类型实例
- 客户端使用事务函数时,函数参数只写参数列表就行了,那个接口实例不需要
- 不能访问全局变量
- 可以调用接口原有的事务函数
- 扩展事务不是接口的成员,不可以在服务端的select case中处理
举例,比如原来定义了接口my_interface。
interface my_interface {
int mul(int a, int b);
};
原来只有乘法事务,现在要扩展一个阶乘(factorial)功能,如下:
extends client interface my_interface: {
int factorial(client interface my_interface self, int n) {
int ret = 1;
for(int i=n; i>=1; ++i) {
ret *= i;
}
return ret;
}
}
完整的程序例子
#include <stdio.h>
#include <platform.h>
/* 定义接口 */
interface my_interface {
int mul(int a, int b);
};
extends client interface my_interface: {
unsigned int factorial(client interface my_interface self, unsigned int n) {
unsigned int ret = 1;
for(int i=n; i>=1; --i) {
ret *= i;
}
return ret;
}
}
/* 接口客户端 */
void task1(client interface my_interface i) {
int ret = i.mul(2, 3);
printf("task1: 乘法结果是%d\n", ret);
ret = i.factorial(4);
printf("task1: 4的阶乘是: %d\n", ret);
}
/* 接口服务端 */
void task2(server interface my_interface i) {
while(1) {
select {
case i.mul(int a, int b) -> int ret:
ret = a * b;
printf("task2: 处理mul(%d, %d)事务\n", a, b);
break;
}
}
}
int main () {
interface my_interface i; // 定义一个接口实例,传给客户端和服务端
par {
task1(i);
task2(i);
}
return 0;
}
执行结果为
task2: 处理mul(2, 3)事务
task1: 乘法结果是6
task1: 4的阶乘是: 24