本文最早刊登于2014.12和2015.1月的《无线电》杂志上,对Galileo Gen2的内部实现做了分析。
转载请注明原文出处:CSK.Blog : http://www.csksoft.net/blog/
在Intel发布首款兼容Arduino开发接口风格的x86开发板Galileo的不到一年内,推出了它的升级版本Intel Galileo Gen2。顾名思义,Gen2即英文Generation 2(第二代)的缩写。从一个简单的照片对比中,我们就可以感受到两者的不同:
从开发板实物的直接对比上,我们可以看到Galileo Gen2的左侧布置了不少小芯片,似乎比前一代复杂了不少。那么它到底与前一代的主要区别在哪里?到底哪些地方得到了提升呢?
当然,这些问题的答案一部分Intel已经为我们解答了:在Intel的官方网站中给出了Galileo Gen2相比前一代的硬件配置的变化。但仅仅硬件配置的不同却远不足以解答以上的这些问题。这些硬件配置具体带来了那些实际的性能和特性改变?在底层设计上,Galileo Gen2具体是怎么做的?
为了将这些问题弄清楚,在没有更多现成资料的情况下,最直接的手段就是亲自去实践了。
同时,我们知道,Intel Galileo虽然是一块兼容Arduino规范的开发板,但其背后基于x86的平台特性又不断地在提醒我们:这又不仅仅只是一块Arduino兼容系统而已。那么,隐藏在兼容Arduino接口规范外表下的Galileo的真实实力又该如何充分展现出来呢?在本文的上一篇《Intel Galileo 开发板的体验、分析和应用》[1]当中,我已向大家简单的介绍了Intel Galileo的底层实现、基于yocto环境的Linux系统开发等话题。在这篇文章中,我将继续深入这些话题。向大家分享Intel Galileo不仅仅作为一个简单的Arduino板的那些功能。
图:在Intel Galileo中安装Debian Linux发行版,运行图形桌面
我们先从外观的比对入手,来看看Galileo Gen2与上一代相比的变化。正如在前文中大家已经知道的,Galileo Gen2相比前一代拥有更大的尺寸,以及看起来更加复杂的电路实现。
除了这2点外,从外观对比上,我们还可以发现如下的不同:
图:Galileo Gen2与上一代的对比
与上一代Intel Galileo一样,Gen2也提供了可以连接USB外设的USB Host接口。当我们知道,上一代Galileo采用的是micro-usb规范的插座来引出USB Host信号。虽然Intel可能是从为了减小PCB尺寸的角度设计的,但这给使用者带来了很大的不便。
就像上一篇文章[1]中介绍的,为了能够让Galileo连接外部设备。用户必须准备一条micro-usb转USB母座的所谓USB OTG线来完成该功能。而在Gen2中,很高兴Intel直接把标准的USB母座设计在了PCB上。这给使用上带来了非常大的便利。
图:自制的3.5mm音频接口的RS232串口
在Gen2当中,由于串口使用的是TTL电平规范,因此可以通过杜邦线与网上很容易买到的usb串口设备连接:
图:Gen2使用2.54mm排针引出TTL串口信号
虽然这样的设计基本是目前开发板的标配,但还是可以看出Intel在Galileo易用性的改善上是下功夫的。
位于Gen2上那些密密麻麻的小芯片也是区分于上一代Galileo的主要标志。这些芯片主要分为如下几个类型:
我将在后文具体介绍这些芯片的用途。
Intel官方所给出[2]的配置变化是这样写的:
为了方便大家对比,并更加具体,可以通过如下表格来参考:
表: Galileo Gen2硬件配置改变,整理自[2]
此外,在具体实现上,Galileo Gen2对于一些外设的芯片选取也有不同的地方。例如用于采集模拟信号的ADC芯片:
从ADC的芯片选取上,Gen2似乎用了一个性能反而比前一代弱的芯片。但事实是否如此呢?这将在后文给出评价。
Galileo Gen2与第一代Galileo相比仍旧采用相同的Quark X1000 SoC,并具有相同的主频。内存也同样是256MB。相比这些大家经常会关注的重点配置,Galileo Gen2主要在一些“外设”上做出改变。
随着Galileo Gen2发布的还有新版本的Arduino IDE以及配套的库函数,此外还有新版本的基于Yocoto发新版。这里我重点介绍新版本x86 Arduino库函数所提供的如下几个接口:
这些接口并不是标准Arduino库中具有的,因为无法在标准的AVR Arduino中使用。但它们给使用Galileo提供了新的可能性。
用于每隔一定的微秒(microseconds参数)后,调用由用户程序提供的callback函数。可以用来实现虚拟的定时器中断功能。
该接口可以使得Galileo具有理论上无限多的定时器(timer),可以用来产生诸如PWM、舵机控制信号等任务。
关于这2个接口的工作原理和能实现的具体性能提升将在后文介绍。他们的作用于标准的digitalWrite和digitalRead一致。但就像名称上提示的那样,这是一个高速的IO操作版本。
使用他们可以实现更快的IO读取/写入性能。
在本文的后续部分,将给出使用这类接口的具体性能。
那么Galileo Gen2做出的这些改变具体意味着什么?撇开上表中很容易知道的具有更宽的供电范围、通过以太网供电这些部分。这里重点关注的是Intel对于Galileo GPIO性能的改善。
为什么在Galileo Gen2中,Intel放弃了先前采用的IO扩展芯片CY8C9540A?从功能上来说,这款芯片可以实现40路的IO引脚输出/输入。并且还可以产生PWM信号。而CPU只需要通过一个简单的I2C总线与他通讯,就可以让Galileo模拟出采用AVR芯片的Arduino开发版的功能规范了。采用这样的芯片,无疑使得电路乃至软件的设计都达到了最简。相信这也是一开始Intel采用该方案的原因。
而在Gen2中,Intel断然放弃了这样的做法。采用了更加折腾的方式:
图:相比前一代Galileo对IO的实现(A),Galileo Gen2的实现(B)令软硬件的设计复杂了不少
Galileo Gen2对IO的实现改变可以通过上图的对比有直观的认识。可以看出,Gen2的实现令软硬件的设计都复杂了很多。我们有理由相信,Intel做出如此复杂的修改的背后,一定有着很强的理由。
而我认为,这个理由就是:可以大幅的提高IO性能。为了证实这一点,就需要我们通过Gen2与上一代Galileo做对比试验,得到具体的性能数据。
为此,我选取了如下的使用场景来进行该对比试验。这些使用场景都是大家在使用MCU控制外部系统中经常遇到的:
下文便是Galileo Gen2与前一代的具体对比。这里先给一个结论:通过试验证实,Galileo Gen2在上述场景下的IO性能均远超上一代Galileo。
IO快速的电平切换在现实使用中非常常见。比如使用GPIO来模拟SPI信号时,就需要产生SPI协议约定的时钟信号。如果IO的输出性能好,就可以实现出很高速的SPI通讯,从而提高系统的工作效率。
实现IO快速电平切换非常容易,可以通过如下Arduino语句完成:
void loop()
{
digitalWrite(pin, LOW);
digitalWrite(pin, HIGH);
}
为方便测试,我直接通过Arduino IDE来编写程序,读者们也可以自行测试验证。
图:单一IO快速电平切换的程序
为了能够测量IO实际的性能,就需要借助工具来完成。通过Arduino IDE将程序下载进入Galileo后,使用示波器来观察12号IO引脚上产生的波形。
图:使用示波器观察Galileo IO输出波形。图中将示波器探针与12号IO口连接
在连接IO口后,程序产生的波形就可以被示波器观测到了。由于IO口循环的进行高低电平的切换,自然在示波器中可以看到的是一个完美的方波信号。而我们需要关注的是这个方波的频率。频率越高,自然说明IO口的输出速度快,性能就越好。
图:示波器中观测到的IO口产生的信号波形
分别将同样的程序在Galileo Gen2与上一代Galileo中执行,并测量对应的波形频率。我们可以得到如下的测试数据:
从上表来看,Galileo Gen2的性能相比前一代用天壤之别来形容一点都不为过。IO的写入性能最快竟然有2000倍以上的提升!也难怪Intel宁愿令软硬件设计变得更加复杂也需要采用原生GPIO了。至于为何会有如此大的性能提升,就留给读者思考了。
图:在示波器中对比Galileo Gen2与上一代产生的信号
不过,上述观测到的2000倍的性能提升还并不是Galileo Gen2的真正性能。前文提到,在Intel实现的Arduino库中,还提供了如下的接口:
void fastDigitalWrite(register uint8_t pin, register uint8_t val);
如果使用这个高速接口来实现上述的试验,性能又会有提升吗?这里我使用如下的代码进行测试:
void loop()
{
fastDigitalWrite(pin, LOW);
fastDigitalWrite(pin, HIGH);
}
为了使用该接口,需要将对应的IO口设置为OUTPUT_FAST模式,比如:
pinMode(pin, OUTPUT_FAST);
修改测试代码后,得到的结果如下:
由于Galileo一代实际并不支持fastDigitalWrite接口,库函数会自动调用digitalWrite。可以发现,fastDigitalWrite相比标准的digitalWrite有大约1.4-1.5倍的性能提升。
与IO写入相对应的,从IO端口读取一次数据的耗时也是经常关心的问题。快速的IO读取常用于对外设硬件的信号轮询上。由于读取操作并不会对外界产生信号,因此无法使用示波器来测量。这里我采用如下代码间接地测量IO读取的性能数据:
int testPin = 0;unsigned long lastTs;
void setup() {
pinMode(testPin, INPUT);
Serial.begin(115200);
}
void loop() {
lastTs = millis();
int counter = 0;
while (counter++<10000) {
digitalRead(testPin);
}
float khz = (float)counter/(millis() - lastTs);
Serial.print("Read spd = ");
Serial.println(khz);
}
上述代码会记录对testPin所指IO端口读取10000次的用时,并换算出单位毫秒内对IO口读取的次数。与之前类似的,这里我也使用Intel新增的fastDigitalRead接口,测试高速版本的IO读取接口的性能。最终,可以得到如下的对比性能数据:
从上述数据看到,对于IO的读取操作,Galileo Gen2相比上一代最快有近4000倍的性能提升!
在某些应用场景下,需要对IO端口的工作模式进行快速的切换。比如对于单工通讯的SPI总线,在宿主发送数据时候,数据IO工作在输出模式。而在宿主发送完数据后,为了快速读取到从属系统的数据,宿主系统需要将数据IO切换为输入状态。较快的模式切换速度,自然也意味着更高效的系统通讯。
因此,对于某一个IO口进行工作模式的切换,也成了这次测试的关注点。
我使用如下的代码并配合必要的外部上拉电阻进行该项目的测试:
void loop() {
pinMode(12, INPUT_PULLUP);
pinMode(12, OUTPUT);
}
为了测量性能,这里也仍旧使用示波器观测波形。从测量得到的波形可以看出,Galileo在切换IO端口不同模式时的耗时是不同的。
图:进行IO模式切换性能试验观测得到的波形
对2个版本的Galileo进行测试后,最终得到如下数据:
从测试结果看,Galileo Gen2的性能仍旧由于上一代Galileo。但并没有之前的测试那么的显著。至于为何这里的差距不明显,这也留给读者朋友门思考了,可以从pinMode在Galileo的实现机制入手探讨。
对ADC端口读取的速度会直接影响信号采集的质量。根据奈奎斯特采样定理,为了能复现目标信号,信号采样频率必须大于被采样信号频率的两倍。因此,ADC的读取速度直接限制了系统所能采集的目标信号频率。这也自然成为了测试的关注点。与IO口电平读取实验一样,ADC的采集速度无法使用示波器等外部测量仪器确定。这里我使用如下的代码辅助采集性能数据:
int sampleCount = 0;
unsigned int lastSampleTS;
void setup() {
Serial.begin(115200);
lastSampleTS = millis();
}
void loop() {
sampleCount++;
int a = analogRead(A0);
unsigned int currentTS = millis();
if (currentTS - lastSampleTS >= 1000)
{
float sampleRate = (float)sampleCount*1000.0f/(currentTS - lastSampleTS);
Serial.println(sampleRate);
sampleCount = 0;
lastSampleTS = millis();
}
}
上述程序会记录1秒内,对ADC的读取次数,并将解决通过串口发送至PC。
图:ADC采集速度的测试情况
分别对Galileo Gen2和第一代进行测试,可得如下的结果:
在前文中,我提到Gen2采用了与前一代Galileo不同的ADC芯片。并且从指标上看是略低的。当从实际测试上看,Gen2的ADC采样速度反而高于前一代。但是却远没有达到芯片所能实现的1MSPS的性能。
以上我介绍了对Galileo Gen2与上一代在四项测试场景下的性能对比情况。这里做一下总结:
Galileo Gen2在这四项测试中,均优于前一代。并且在GPIO的输出和读取部分中,具有大幅领先的优势。其测试结果汇总如下。
那么为何Gen2会具有如此大的性能提升呢?在Linux下程序又是如何直接与硬件打交道,实现对于GPIO的控制的?这些便是下文介绍的重点。
此外,相信细心的读者发现上表中Galileo Gen2的测试数据中基本都有2个数据项:Pin12和Pin6。并且这2列的数据还各不同。为何在Galileo Gen2下不同的GPIO端口的性能还会存在不同?这里便涉及到Quark X1000 SoC这颗芯片内部对GPIO的实现细节了。在后文中也将探讨这个问题。
这个章节中我将分别从硬件和软件角度简要的介绍Galileo Gen2的实现,由于篇幅,我只重点涉及对于外部IO接口部分的细节。通过这里的分析对上文的性能测试结果给出解释。
在Galileo Gen2原理图[5]中提供了如下框图:
图:Intel Galileo Gen2硬件框图,来自[5]
相比Gen2,上一代Galileo的GPIO和PWM信号都是由同一个IO扩展芯片完成的,因此在电路实现上反而显得相对的简单。
图:在Galileo Gen2和上一代版本接口部分的对比
我们重点关注Galileo Gen2对于IO外设的设计实现。也就是框图中右侧的那些部分。可以看到,兼容Arduino接口的GPIO信号的确是直接从Quark SoC芯片中引出。但是,对于PWM、ADC部分,Galileo仍旧借助着外部芯片实现这些功能。并且图中还有GPIO Exp, MUX SHIFT这些芯片存在,并且他们之间还使用了I2C总线与Quark SoC芯片连接。这些芯片的作用是什么呢?这里将依次为大家分析。
从前文性能测试的结论我们可以观察到,虽然Galileo Gen2使用了原生GPIO后,对于IO口写入和读取的速度都有了大幅的提升,可以实现几百khz的工作频率。但是相比而言,在切换IO工作模式时速度就没那么快了。为了解释这一现象,就要从Galileo Gen2对于IO口功能切换的实现说起。
我们知道,Arduino是允许同一个GPIO引脚具有不同的功能的。在AVR芯片内部设计了可以通过CPU配置的信号切换电路来实现对不同外设功能的切换,使得同一个IO引脚随着程序控制变成GPIO、PWM输出、串口、I2C或者ADC。这种切换电路就好比是现实世界当中的机械开关。只不过在芯片内,他们是采用MOS管来实现的。
图:为了实现IO口功能切换,就需要引入等效于机械开关的信号切换机制
为了兼容这一点, Galileo也必须引入功能切换机制。但不同于AVR,Galileo需要额外使用外部的硬件来实现。它采用了叫做多路复用器(multiplexer,简称MUX)的芯片完成的。
我们在Gen2上看到的这些小芯片便是MUX:
图:Galileo Gen2中用于切换信号的MUX芯片74LVC1G157[6]
这些小芯片型号为74LVC1G157[6],实现了对于2路数字信号的二选一切换,而为了确定具体切换其中哪一路,就需要外部系统提供一个选择信号来明确。/
图:Galileo Gen2原理图[5]中,74LVC1G157的原理图部分
在上图中74LVC1G157的原理图部分,我们看到对于输入MUX1_I1和MUX1_I0的选择切换是通过MUX1_SEL这个信号控制的。
当然,MUX1_SEL是由Quark CPU控制产生的,从实现上,他应该也是通过CPU的一路GPIO信号实现。但是在Gen中使用了十多个这样的MUX芯片,在Quark SoC芯片上并不能提供那么多的GPIO引脚。因此Galileo使用了IO扩展芯片来完成对于这类MUX芯片的控制,而将Quark芯片上的GPIO这宝贵的资源直接用于Arduino IO口。
这便是在前文给出的硬件框图中的GPIO Exp芯片的真正作用:
图:Galileo Gen2使用外部的GPIO扩展芯片来实现对于MUX芯片的控制
从原理图中可以看出,Galileo Gen2一共使用了3块名为PCAL9535[7]的16口IO扩展芯片,以及借用了部分PWM输出芯片PCA9685的部分IO输出来实现对于MUX芯片的控制。但这些扩展的IO口并不都是用于控制MUX芯片,其中也有用于对于IO口上拉(实现pinMode中上拉输入模式)和IO电平在5V和3.3V之间切换的任务。
相比从Quark SoC芯片的原生GPIO,CPU是通过I2C总线和这些扩展芯片来操作这些IO信号的,这样的做法与第一代Galileo中对Arduino IO的控制一样。相比原生GPIO,通过I2C的控制自然在速度上会慢很多。这便解释了为何在性能测试中我们发现IO口模式切换慢很多的原因.
图:Galileo Gen2中使用的IO扩展芯片PCAL9535
Galileo Gen2虽然直接从Quark SoC芯片引出原生的GPIO信号,但这并不意味着这些GPIO信号可以简单的与外部的系统互联。这主要是由于Quark芯片采用的是3.3V的电平规范,这与使用5V电平规范的Arduino接口不兼容。
为了实现电平的兼容,并保护Quark芯片的GPIO引脚免遭外部系统损坏,就需要进行电平的转换工作。由于每一路IO口都需要进行电平转换,因此就需要使用比较多的电平转换芯片来完整这一功能。
为了进行电平转换,一般会采用缓冲器芯片来实现。不过缓冲器一般都是设计成单向的,单个缓冲器并不能实现IO口即可以输出又能输入的需求。因此每个IO端口就需要2个缓冲器分别完成对输出信号和输入信号的电平转换。对于13个IO口的Arduino来说,这就差不多需要26个缓冲器做电平转换。索性的是目前的缓冲器芯片中集成了多路的信号通道,因此实际并不需要26个独立的芯片。但即使这样,我们还是能在Galileo Gen2上看到那些密密麻麻的芯片,他们主要就是这些做电平转换用的缓冲器了。
图:Galileo Gen2原理图中进行IO信号电平转换的部分
从Galileo Gen2的原理图中我们可以很清楚的看到,对于IO信号的输出部分,使用了SN74LV541锁存器以及SN74LV125a总线用缓冲器实现电平转换。又采用了74LVC126A作为对输入信号采集前的电平转换。这样的设计虽然繁琐,但倒不会对IO的输出性能产生显著的影响。
图:Galileo Gen2中用于IO电平转换的芯片
在前文中已经提过,Galileo Gen2的PWM信号是通过外部芯片PCA9685[4]完成的。通过该芯片的数据手册可知,它主要用于LED、LCD背光的控制。将它用在Galileo中当作PWM输出使用,虽然不是这个芯片的直接设计使用场景,但的确很符合他的功能定义。从原理图中,可以看到该芯片的PWM输出端口被命名成LED0,LED1等等,这在初次看原理图时比较容易误导人。此外,正如前面提到的,Galileo也使用了这款PWM芯片来完成IO扩展的功能,用于控制MUX。
图:Galileo Gen2采用了一款LED背光驱动芯片来实现PWM输出
Quark SoC通过I2C总线与PCA9685通讯,进行配置PWM的频率、占空比等操作,查询数据手册可知,该芯片可以输出频率为40Hz到1000Hz范围。这样的频率范围对于电机控制来说一般是够用了。但如果希望使用PWM产生音乐,则会遇到问题。为此,在Galileo实现的Arduino库中,产生音乐的Tone库是直接采用fastDigitalWrite配合Linux提供的多线程机制通过纯软件实现的。
图:PCA9685芯片在Galileo Gen2板上的位置
这里给出Galileo Gen2上主要芯片的作用和分布:
前面介绍了Galileo Gen2对于IO外设的大致实现情况。那么站在软件的角度看,Galileo上运行的程序是如何控制这些IO口的呢?这里我将重点介绍程序对于Quark SoC中原生GPIO的控制实现原理。并解答在前文评测中为何2个不同的IO口具有不同的性能根本原因所在。
为了了解程序是如何控制GPIO的,最根本的手段就应该从硬件的原理出发,了解软件所运行的目标硬件的构造。这些信息可以在Quark SoC芯片的数据手册[8]中找到答案。
在位于手册的介绍部分有一副介绍Quark SoC的系统框图,其中描绘了Quark芯片各种片上外设与CPU的连接拓扑结构。而其中有一个点值得我们关注:
图:Quark SoC数据手册[8]中描绘的硬件框图,可发现有2种GPIO设备
在图中,我们发现AMBA Fabric与Legacy Bridge都具有GPIO设备。也就是说,难道Quark SoC有两种不同的GPIO接口吗?
通过进一步的深入分析,答案是肯定的。继续阅读数据手册,更多的细节揭示了出来。位于手册[8]的19.0章节中有一段话说明了其中的奥秘:
“The SoC provides a total of 16 GPIOs that are split between the Legacy Bridge (D:31 F:0) and the GPIO controller (D:21 F:2).”
即Quark SoC一共提供了16个GPIO信号,其中一部分位于名为Legacy Bridge的设备,另一部分位于专门的GPIO控制器中。而这里的Legacy Bridge在手册中介绍,则是对于传统PC构架中的外设,比如8259中断控制器、8254定时器、RTC以及GPIO控制器的兼容实现。而AMBA Fabric则是新增加的规范,实现了I2C和GPIO的功能。这2个模块在Quark SoC当中,被实现成为了2个独立的PCI设备,x86 CPU透过PCI总线与他们进行交互通讯。在传统PC当中,他们可能就会被实现成独立的PCI卡外设,或者直接继承在了主板的南桥芯片当中。而对于Quark SoC单芯片解决方案,这些外设就被集成在了一起,但为了保证兼容性,从软件的角度看,他们还是标准的PCI外设。
图:在手册中展现的Quark SoC芯片内部的PCI设备树
既然Quark SoC的GPIO端口在硬件实现上被分配到了不同的内部设备中。那么从硬件角度来看,对着2种不同的GPIO的操作是不是存在差异呢?而这种差异是否正是导致前文中观察到Galileo Gen2不同IO口输出/输入性能不同的根源?
在给出这些问题的答案前,先谈谈一般从CPU角度看,操作GPIO这种外设的做法一般是怎样的?
对于熟悉单片机开发的朋友来说,控制单片机芯片的某个IO口高低电平就是一个内存写入操作。比如在AVR芯片中,要给PA0引脚设置高电平用C语言是这样写的:
PORTA |= 0x1;
其中,PORTA是一个对内存指针进行读写操作的宏,将宏展开后,对应的最本质代码是:
*(0x25) |= 0x1;
也就是只要往地址是0x25的内存的第一位设置为1,此时PA0引脚就变成了高电平。
这种通过对内存读写完成外设操作(IO)的方式被称为内存映射输入输出(MMIO)。而这种方式并非是单片机所特有的。目前的计算机体系主要是基于冯诺伊曼结构[10]/哈弗结构[11]。对这些计算机系统体系的介绍当然超过了本文的范畴,搬出这个概念的目的是为了说明:从现代处理器结构看,为了操作外部设备,主要手段就是内存访问操作。
图:基于哈弗结构的处理器,工作过程中对外部数据的读取均为访存操作
对于运行在处理器当中的程序来说,要对外部的数据进行读取、写入或者操作具体的硬件(即IO)操作,从体系结构上来说,本质都是访存操作。这里的访存就是内存访问的意思。当然,所谓的内存并不是平常我们所说的RAM那么狭义。
对于x86构架的Quark SoC来说,要操作GPIO也不无例外的是基于这种方式来进行的。只不过相比AVR这种简单的芯片,x86更加复杂一点:它提供了多种地址空间用于CPU对不同类型设备进行操作。而最多见的就是传统意义上内存地址,RAM地址空间,以及操作外设的IO地址空间。
图:Quark SoC中的内存和IO地址空间
早在Intel 8086处理器中就已经提供了内存地址空间和IO地址空间的划分。虽然如今现代的精简指令集处理器,比如ARM,为方便软件编写,已不再提供多地址空间的划分并将所有的内存以及基于MMIO操作的外设都编入统一的地址空间。但x86出于对兼容性的考虑,IO地址空间被保留了下来。
对于不同地址空间下的访存操作,x86处理器提供了不同的指令来完成。对标准的内存地址空间,是通过mov指令进行的。而对于IO地址空间的数据操作,则需要使用in/out指令。
回到Quark SoC的GPIO实现上,由于Legacy Bridge用于模拟兼容历史上PC构架中的外设,因此位于Legacy Bridge的GPIO设备被编入了IO地址空间中。程序需要使用in/out指令来操作它们。而位于AMBA Fabric的GPIO,则使用了PCI设备的规范,将他们编入了内存地址空间的MMIO部分。因此程序只需要简单的使用mov指令进行操作即可。
分析到这里,自然前面的问题也得到了解答。无论是硬件还是软件上,对于这2种不同的GPIO的操作就是不相同的。因此,他们能实现的性能会有差异也就不足为奇了。
我们知道Galileo中运行的Arduino Sketch程序本质上是一个标准的Linux用户态程序。它与采用AVR芯片的Arduino板不同,这里的程序并不具有对于硬件的直接控制权。那么Sketch程序里面是怎么控制GPIO的?它可以像单片机那样,直接通过对GPIO的MMIO进行读写操作吗?
通过阅读Galileo配套的Arduino库源代码以及Galileo使用的Linux 内核源代码可以找到这个问题的答案。
位于代码wiring_digital.c的代码的digitalWriteSetVal中,展现了Galileo下实现GPIO控制的核心。该函数直接被我们熟悉的digitalWrite所调用。
图:Galileo的digitalWrite函数实现片段
从代码可以看到,对于某个GPIO的输出操作最终将调用sysfsGpioSet函数,而该函数的功能等同于往如下的Linux目录的文件中写入1:
/sys/class/gpio/gpio[X]/value
为了证实这一点,可以登录到Galileo的Linux终端,进入到上述目录进行验证:
图:Galileo Linux系统中将GPIO的操作透过sysfs机制可以让用户态程序使用
进一步分析代码,也会发现,对于IO的读取,比如digitalRead、analogRead,以及PWM输出控制analogWrite,均是通过类似的对Linux文件系统的操作实现的。在variant.h文件中,可以找到这些对应文件路径的定义:
当然,这里看到的这些“文件”并不可能是真正存储在存储器上的真实文件。他们是由Linux内核产生的虚拟文件对象[12],用于用户程序与Linux内核程序交互用的。因此,真正对GPIO的操作是存在于Linux内核驱动当中。
将搜索范围转移到Galileo使用的Linux内核源代码,经过分析,对于GPIO操作是由如下2个驱动程序实现的:
其中,intel_qrk_gip_gpio.c实现了对于前文提到的位于AMBA Fabric上GPIO设备的驱动。对于GPIO输出的操作,可以在其中的intel_qrk_gpio_set函数找到:
从上面的代码片段可以看到,代码最终会调用iowrite32()来进行GPIO操作。该函数本质上等效于:
*reg_data = val_data | BIT(offset % 32);
这便是前文提到的MMIO操作,就像AVR上操作GPIO一样。
再来看gpio-sch.c代码,它实现了对于Legacy Bridge中的GPIO的驱动。并且正如前面提到的,由于Legacy Bridge的设备映射在IO地址空间,因此代码通过in/out指令来驱动这些GPIO:
上述代码片段实现了对GPIO的输出功能,其中的核心就是outb()语句,它是对out指令的封装。
通过上文的分析,大家知道了在Quark SoC中提供了2种GPIO接口,并且由于他们的内部硬件实现方式不同,因此在使用上便具有了不同的性能。那么,在Galileo使用中,哪些IO口是与上文性能评测中的PIN12一样具有较高的写入性能的?哪些IO口又是性能稍弱的呢?对此,我们同样可以在Galileo库源代码中找到答案:
图:Galileo库源代码variant.h中定义的Arduino IO口在Quark SoC中的实现方式
在文件variant.h中可以找到上面的宏定义,我们可以发现Galileo把IO口定位成了两大类
通过进一步阅读,这里的SC是South-Cluster的意思,他们直接对应Quark SoC硬件中通过AMBA Fabric实现的GPIO。而NC是North-Cluster的缩写,这部分对应的GPIO则是通过Legacy Bridge引出的。
通过测试证明,这2组IO口在上文的测试当中的表现中,同一组的IO性能都是非常接近的。而不同组的IO的性能就存在差异。这便证明了之前对于GPIO性能不同的全部猜测。
此外,也可以发现,在上述定义中缺少对IO7和IO8的定义。通过参考Galileo Gen2原理图可知,这2组IO是通过IO扩展芯片提供的。因此他们的性能相比最差。
这里我将所有Galileo中提供的IO口与他们对应的实际硬件实现方式整理出来,方便大家参考。
至此,我们了解了从Arduino Sketch用户程序到Linux内核直到控制GPIO的控制通路:
图:完整的从Arduino Sketch程序到底层GPIO控制的通路
从上图可以看出,在Galileo当中,如果通过digitalWrite操作一个GPIO端口,就需要经过用户态程序、文件系统、内核驱动这几个环节,最终再通过MMIO或者IO操作进行真正硬件GPIO的操作。
相比单片机的做法,这样操作GPIO效率自然会低很多。那么,在Galileo中我们可以像AVR那样直接在Arduino Sketch程序中直接操作内存或者io端口来设置GPIO吗?
为了加速对GPIO的操作效率,Galileo使用了用户态直接IO驱动的手段,使得用户态的应用程序也可以像内核程序那样直接通过内存读写或者IO操作来驱动GPIO。Galileo库中提供了快速版本的GPIO操作函数fastDigitalRead/fastDigitalWrite。
在前文的性能评测中我们也看到了使用该接口后,IO操作的速度比前面透过sysfs的方式快了不少。
为了实现用户态直接进行IO设备的驱动,Galileo利用的是Linux Kernel提供的UIO(用户态IO,Userspace IO)的机制[13]。UIO在Linux中被实现为一个标准的驱动程序,它可以将原本只有内核程序访问的MMIO以及IO地址空间的设备,透过内存映射mmap或者给IO操作授予用户态程序权限,使得用户程序也可以像内核驱动那样直接操作IO外设。在Galileo库源代码的fast_gpio_nc.c以及fast_gpio_sc.c中,我们可以分别找到Galileo对这种机制的应用:
图:Galileo库利用UIO机制[13],实现高速的GPIO操作。(a)使用内存映射后,操作MMIO的AMBA Fabric上的GPIO。(b)获取访问权限后,在用户态使用in/out指令访问Legacy Bridge的GPIO。
通过UIO机制,高速版本GPIO操作函数绕过了标准的通过sysfs文件系统的GPIO控制通路,像单片机那样直接驱动底层IO。因此获得了更大的性能提升。
在分析介绍了Galileo Gen2的性能情况以及内部实现后,这里分享一些我在使用Galileo Gen2中的经验。
我们知道,Galileo是基于Linux系统的,并且Galileo的Arduino开发环境本质上就是一个C++语言环境的IDE。因此,我们可以在Sketch中调用Linux提供的其他系统调用以及库函数来完成标准Arduino接口所不可能完成的事情。
在Linux 终端下,可以使用Shell命令ps来获取当前系统正在执行的进程列表。在Galileo上,我们可以通过如下的代码,在Arduino Sketch中调用ps命令,并将结果暂时保存在文件中,并最终通过串口发送出来:
void setup() {
Serial.begin(115200);
}
void loop() {
char buf[1024];
system("ps > /home/root/processlist.txt");
FILE *fp;
fp = fopen("/home/root/processlist.txt", "r");
Serial.println("Current Process List");
while (fgets(buf, sizeof(buf), fp)) {
Serial.print(buf);
}
fclose(fp);
delay(5000);
}
上述代码首先调用ps命令获取当前Galileo正在执行的进程列表,并保存为外部文件processlist.txt。随后,程序调用标准的C函数文件操作接口,并借助Serial类将数据通过串口打印出来。在PC上可以通过串口调试器看到Galileo正在运行的进程信息:
图:通过串口调试器看到的本例子输出
在Arduino开发环境中,程序默认只有一条执行线程。为了进行多线程开发,在标准的基于AVR的Arduino开发板上,需要借助各类多线程库来实现。而在Galileo上,则可以直接利用Linux系统的POSIX多线程特性,使用pthread库进行多线程的开发。下面的程序展示了在Galileo中使用pthread多线程机制,将串口输出操作转为后台线程。
在Galileo中,PWM的输出频率设置在了483Hz,这个数值与标准Arduino使用的490hz相兼容。但很多时候,我们需要改变PWM的频率。
这里介绍一种通过操作sysfs进行PWM芯片输出频率配置的方法。在Linux Shell环境下,我们可以使用如下的命令配置Galileo的PWM芯片频率,甚至是直接控制PWM信号输出并设置占空比:
echo -n "3" > /sys/class/pwm/pwmchip0/export
echo -n "1" > /sys/class/pwm/pwmchip0/pwm3/enable
echo -n "62500" > /sys/class/pwm/pwmchip0/pwm3/period
echo -n "31250" > /sys/class/pwm/pwmchip0/pwm3/duty_cycle
上述shell命令将设置3号IO口输出PWM输出频率为13.97khz ,并且占空比50%的波形。在Arduino Sketch中,则可以使用system()函数来执行上述命令。
为了能够直接操作Galileo中运行的Linux系统,最常用的做法是使用Galileo上的串口信号,或者通过以太网,使用telnet或者ssh的方式进行登录操作。
这里介绍直接使用Galileo用于进行Arduino开发的usb虚拟串口进入Linux终端的方式。采用这种方式就无需额外的串口或者以太网设备。仅需要一条USB线即可登录进入Galileo上的Linux系统。
他的核心原理比较简单:通知系统在usb串口上建立一个shell进程。在Arduino Sketch中使用如下语句:
将Sketch加载进入Galileo运行,此时,我们打开putty等终端工具,即可访问到Galileo的Linux Shell:
如果需要对Galileo进行深入的开发,就有可能进行从Linux内核层面的完整的系统定制。为此,Intel选择使用Yocto开发环境方便开发人员产生完整的从内核到rootfs的完整Linux系统。其中的具体开发过程这里就不在重复了,读者们可以参考Intel提供的Quark开发文档[14]。
和其他标准Linux下软件项目开发一样,yocto的使用开发主要都是基于命令行的。作为合格的系统开发人员,使用命令行是必备的素质要求。但这的确给不少新手造成了障碍。这里将介绍Yocto系统中自带的GUI配置系统,可以一定程度上缓解开发难度。
在按照[14]指导的Yocto开发环境的配置过程中,我们会先从Intel网站下载用于Galileo开发的初始yocto开发包,在解包并调用setup.sh配置好系统之后,就可以得到如下的目录构造:
图:用于Galileo开发的Yocto环境目录构成
此时,打开终端,在yocto目录中输入命令:
$ source poky/oe-init-build-env yocto_build$ hob
此时,就可以看到yocto系统自带的图形化配置编译工具hob[15]的主界面。在其中可以通过图形方式勾选需要的编译组件,并且完整整个编译过程。对于其进一步的应用,可以参考文献[16]。
图:使用Hob简化Yocto的使用过程
作为x86的开发版,Galileo除了运行基于yocto开发系统产生的Linux系统外,目前也可以运行稍微做过修改的Debian Linux发行版。运行Debian发行版有一定的好处,比如可以使用apt-get通过网络从软件仓库下载更多的应用来安装,而无需每次通过yocto自行编译。此外,也可以使用RoboPeak USB显示器显示出图形的桌面。这样可以弥补Galileo没有原生图形输出的缺陷,可以将Galileo应用在一些需要GUI人机交互的场合当中,或者可以直接在USB屏幕中观看OpenCV处理摄像头画面的结果。
为了安装Debian发行版,需要一张4Gb以上的TF卡,并从[17]下载RoboPeak专门为Galileo定制的Debian系统镜像即可。
图:配合USB显示屏和集成的触摸屏,在Galileo上运行Debian桌面的效果
这篇文章为大家展现了Galileo Gen2的内部实现以及相比前一代的性能上的提升。并且简要分享了我在使用过程中的一些经验。希望对各位起到抛砖引玉的作用。
[1] Intel Galileo 开发板的体验、分析和应用
http://www.csksoft.net/blog/post/304.html[2] Whats new with the Intel® Galileo Gen 2 board
http://www.intel.com/content/www/us/en/do-it-yourself/galileo-maker-quark-board.html[3] CY8C9540A: 20-, 40-, and 60-Bit I/O Expander with EEPROM
http://www.cypress.com/?docID=31413[4] 16-channel, 12-bit PWM Fm+ I2C-bus LED controller
http://www.nxp.com/products/lighting_driver_and_controller_ics/i2c_led_display_control/series/PCA9685.html[5] Intel Galileo Gen2原理图
https://communities.intel.com/docs/DOC-22895[6] 74LVC1G157 - Single 2-input multiplexer
http://www.nxp.com/documents/data_sheet/74LVC1G157.pdf[7] PCAL9535 - Low-voltage 16-bit I2C-bus I/O port with interrupt and Agile I/O
http://www.nxp.com/documents/data_sheet/PCAL9535A.pdf[8] Intel Quark SoC X1000 Datasheet
https://communities.intel.com/docs/DOC-23092[9] DesignWare IP Solutions for AMBA
http://www.synopsys.com/dw/amba_fabric.php[10] Von Neumann architecture
http://en.wikipedia.org/wiki/Von_Neumann_architecture[11] Harvard architecture
http://en.wikipedia.org/wiki/Harvard_architecture[12] sysfs - _The_ filesystem for exporting kernel objects
https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt[13] Device drivers in user space
http://www.embedded.com/design/operating-systems/4401769/Device-drivers-in-user-space[14] Intel® Quark™ BSP Build and Software User Guide
https://communities.intel.com/docs/DOC-22476[15] Hob is a graphical user interface for BitBake
https://www.yoctoproject.org/tools-resources/projects/hob[16] Build custom embedded Linux distributions with the Yocto Project
http://www.ibm.com/developerworks/library/l-yocto-linux/index.html[17] 为Galileo定制的Debian系统
http://www.robopeak.com/blog/?p=633