友情提示:380元/半年,儿童学编程,就上码丁实验室。
我是潘,曾经是个工程师。这是 “Arduino 公开课” 系列的入门教程。之前介绍了飞利浦的 I2C 协议,从本课将开始介绍 SPI 总线;但 SPI 不能称为协议,因为与 I2C 不同,它并没有一个机构为其制定任何标准,也正因如此,SPI 运用要比 I2C 灵活。有任何疑问请在评论区提出,我会逐一回答。
与 I2C 两根数据线相比,要用到4根数据线的 SPI 好像更复杂了。实际并非如此。首先,SPI 总线没有 I2C 复杂时序设计。其次,I2C 寻址和数据操作都是在两条数据线上的,而 SPI 总线上每个从属设备都有一条物理地址线与主机连接,谁的地址线电位低,谁就是操作对象,不容易出错。
先就此打住,实践中获得真知是 ArduiCo 的理念 ,因此我先从最简单通信方式讲起,直到我们对 SPI 的运用了然于心后,再深入总线内部,与 I2C 来一次大对决。
不过,我们还先得从理论讲起……
SPI 是 “Serial Peripheral Interface” (串行外设接口)的缩写,通信由 3 条 数据线和 1 条寻址线来负责:
MOSI,“Master-out, Slave-in”,主机发送,从机接收
MISO,“Master-in, Slave out”,主机接收,从机发送
SCK,“Serial clock”,时序
SS,“Slave-select”,从机寻址,1 条数据线接 1 个从机,不能连接多个;低电平时,该从机进行读写操作。
发现问题了吗? I2C 两天数据线是 1 根时序(SCL)和1 根数据(SDA),单根 SDA 要负责发送和接收,而 SPI 的接收和发送是分开两根数据线的。这意味着,后者可以工作在“全双工”(同时收发)状态,而 I2C 只能工作在半双工(要么接收、要么发送)状态。另外,I2C 的 SDA 还要负责寻址,会导致切换设备时产生延迟。
在这系列教程中,我们约定 Arduino 是主机,所有传感器等设备都是从机。对 Arduino UNO来说, 已经默认了 4 个 I/O 为 SPI 接口:
MOSI,D11;
MISO,D12;
SCK,D13;
SS,寻址端默认为 D10,但连接多个设备时,可将D9、D8……只要你有足够的数字端口,都可以设置为寻址端。
不同版本的开发板默认接口不一样,Mega 的 MISO 为 50, MOSI 为 51, SCK 为 52 and SS 通常是 53。具体请参考官方文档。 SPI 连接多个设备的方式如下:
一个从属设备需要一根 SS 寻址线,这是SPI的优点,也是缺点,因为 Arduino 的接口有限。另外,请记住,SS 低电平时,对应设备进入操作状态,所以连接多个设备时,SS 尽量设置上拉电阻。
SPI 设备通信以 byte 为单位,每次可以传输一个 8bits 数据,相当于 0~255 值。数据结构如下:
SPI 通信时要先定义从 byte 那一边读起,如图所示,左边为 MSB,右边是 LSB,大部分情况我们都会选择从 MSB 读起。
8bits 二进制数据除了数值,还能代表8位开/关的控制命令,这个参数设定非常多样,具体要看 DataSheet 的图例。我们现在就拿数字电位器 MCP41xxx(单通道) /MCP42xxx(双通道) 来举例:
SI 就是 MOSI,Arduino 向设备发送数据。要操作这个电位器,起码要发送,COMMAND Byte 和 Data Byte 两个字节的数据。
首先,发送一个 COMMAND Byte,C1、C0 bits 用于设置电位器的状态,P1、P0 用于选择通道(仅对双通道的 MCP42xxx 适用)。然后,发送 Data Byte,给电位器设置 0~255 值。
程序怎么写呢?Arduino 已经内置了SPI 库:
1
|
#include “SPI.h”
|
下一步,void setup() 中声明 SS 寻址线的输出状态,记住: SS 为低电平时,对应设备可进行读写操作:
1
|
pinMode(ss,OUTPUT);
|
启动 SPI 总线:
1
|
SPI.begin();
|
我们需要设置从 MSB 读取,还是 LSB 读取,默认是前者:
1
|
SPI.setBitOrder(MSBFIRST);
|
设置完成后,要对设备进行读写,操作逻辑是先把SS拉低,然后 transfer 数据:
1
2
|
digitalWrite(SS,LOW);
SPI.transfer(value);
|
数据传输完成后,将 SS 拉高:
1
|
digitalWrite(ss,HIGH);
|
是否感觉到发送模式很简单?一般来说,要理解 DataSheet 是很费劲(但不困难),但只要通过简单的演示,就变得很容易理解了。
两周前,我专门订购了几块 MCP41010 数字电位器(MCP41xxx,100表示 100KΩ,050表示 50KΩ,010 表示 10KΩ;MCP42xxx系列是双通道的,适合立体声音频控制。另外,同类芯片还有 MCP4162,但市面很少见),含进口税价大概 6.5 元 1 片。为什么要用这块 IC 来讲解,因为它的数据结构是最直观的(DataSheet):
按照 DataSheet 的标识,连接 Arduino UNO 默认的 SPI 端口即可,但 MISO悬空,因为数据电位器没有数据要反馈给 Arduino:
MOSI,D11;
MISO,无,悬空
SCK,D13;
CS(即SS),D10
Vdd,5V
Vss,GND
IC上,剩下的 3个引脚分别是电位器 PB0 端(B)、PW0(Wiper)、PA0 端(A),我们程序要控制的是 Wiper ,相当于旋转电位器的中间引脚,电阻向B滑动,B端和W端的电阻 Rbw 变小,W 端和A端之间的电阻 Rwa 变大,反之亦然。
根据电阻分压原理,我们将 B 端连接到 Arduino 3.3V端口,A 端连接到 GND,然后调整 Wiper,并测量 W端 它与 GND之间的电压:
V = V * (Rwa)/(Rbw + Rwa)
没有看懂?请找本初中物理课本,复习“电阻”那一章。
下面的程序会让Wiper不断向两边滑动,电压不断起伏变化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/*
作者:Ardui.Co
效果:连续改变数字电位器阻值
版本:1.0
更新时间:2017年5月18日
*/
#include “SPI.h”
intss=10;
intdel=200;
voidsetup()
{
pinMode(ss,OUTPUT);//设置 SS 为输出
SPI.begin();// 启动 SPI
SPI.setBitOrder(MSBFIRST);//设置从那边开始读起
}
voidsetValue(intvalue)
{
digitalWrite(ss,LOW);
SPI.transfer(0×11);// 发送控制命令,表示向第一通道的电位器写入数据,即二进制 00010001,参见DataSheet
SPI.transfer(value);// 设置电位器的数值,0~255 (0 ~ 10KΩ)
digitalWrite(ss,HIGH);
}
voidloop()
{
for(inta=0;a<256;a++)
{
setValue(a);
delay(del);
}
for(inta=255;a>=0;–a)
{
setValue(a);
delay(del);
}
}
|
我们会看到电压来回变化: