一.什么是SPI通信接口?
SPI是串行外设接口(Serial Peripheral Interface)的缩写。是美国摩托罗拉公司(Motorola)最先推出的一种同步串行传输规范,也是一种单片机外设芯片串行扩展接口,是一种高速、全双工、同步通信总线,所以可以在同一时间发送和接收数据,SPI没有定义速度限制,通常能达到甚至超过10M/bps。
SPI有主、从两种模式,通常由一个主模块和一个或多个从模块组成(SPI不支持多主机),主模块选择一个从模块进行同步通信,从而完成数据的交换。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起,当存在多个从设备时,通过各自的片选信号进行管理。
二.SPI硬件引脚介绍
SPI通信原理很简单,需要至少4根线,单向传输时3根线,它们是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)和CS/SS(片选):
- MISO( Master Input Slave Output)简称主入从出信号线:主设备数据输入,从设备数据输出;
- MOSI(Master Output Slave Input)简称主出从入信号线:主设备数据输出,从设备数据输入;
- SCLK(Serial Clock)串行时钟:为 SPI 通信提供时钟;时钟信号,由主设备产生;
- CS/SS(Chip Select/Slave Select)为片选信号线:为 SPI 通信提供时钟;从设备使能信号,由主设备控制,一主多从时,CS/SS是从芯片是否被主芯片选中的控制信号,只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。
SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过 SPI 线连接多个从设备的结构,如下图所示:
SPI主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输。
三.SPI通信过程
SPI数据通信的流程可以分为以下几步:
1、主设备发起信号,将CS/SS拉低,启动通信。
2、主设备通过发送时钟信号,来告诉从设备进行写数据或者读数据操作(采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低),因为SPI有四种模式,后面会讲到),它将立即读取数据线上的信号,这样就得到了一位数据(1bit)。
3、主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(缓存长度不一定,看单片机配置),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。
4、从机(Slave)也将自己的串行移位寄存器(缓存长度不一定,看单片机配置)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。
例如,下图示例中简单模拟SPI通信流程,主机拉低NSS片选信号,启动通信,并且产生时钟信号,上升沿触发边沿信号,主机在MOSI线路一位一位发送数据0X53,在MISO线路一位一位接收数据0X46,如下图所示:
这里有一点需要着重说明一下:SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。若只进行写操作,主机只需忽略接收到的字节(虚拟数据);反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。
四.SPI总线时钟
SPI时钟特点主要包括:时钟速率、时钟极性和时钟相位三方面。
(一) 时钟速率
SPI总线上的主设备必须在通信开始时候配置并生成相应的时钟信号。从理论上讲,只要实际可行,时钟速率就可以是你想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。
(二) 时钟极性
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。
CKP可以配置为1或0。这意味着你可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。你必须参考设备的数据手册才能正确设置CKP和CKE。
- CKP = 0:时钟空闲IDLE为低电平 0;
- CKP = 1:时钟空闲IDLE为高电平1。
(三) 时钟相位
根据硬件制造商的不同,时钟相位通常写为CKE或CPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;
- CKE = 0:在时钟信号SCK的第一个跳变沿采样;
- CKE = 1:在时钟信号SCK的第二个跳变沿采样。
五.SPI接口优缺点
- 优点
- 无起始位和停止位,因此数据位可以连续传输而不会被中断;
- 没有像I2C这样复杂的从设备寻址系统;
- 数据传输速率比I2C更高(几乎快两倍);
- 分离的MISO和MOSI信号线,因此可以同时发送和接收数据;
- 极其灵活的数据传输,不限于8位,它可以是任意大小的字;
- 非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。
- 缺点
- 使用四根信号线(I2C和UART使用两根信号线);
- 无法确认是否已成功接收数据(I2C拥有此功能);
- 没有任何形式的错误检查,如UART中的奇偶校验位;
- 只允许一个主设备;
- 没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
- 没有定义硬件级别的错误检查协议;
- 与RS-232和CAN总线相比,只能支持非常短的距离;
六.SPI总线应用示例
SPI(Serial Peripheral Interface)总线的代码示例因平台和编程语言的不同而有所差异。以下是一些不同环境下的SPI总线操作的基本代码片段:
在Linux内核驱动开发中的SPI操作示例(C语言):
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/spi/spi.h>
static struct spi_device *spi_dev;
static int __init my_spi_driver_init(void)
{
// 定义SPI设备结构体参数
static struct spi_board_info board_info = {
.modalias = "my_spi_device",
.bus_num = 0, // SPI总线编号
.chip_select = 1, // 片选线编号
.max_speed_hz = 1000000, // 最大传输速率
};
// 注册SPI主控制器
spi_dev = spi_new_device(spi_busnum_to_master(0), &board_info);
if (!spi_dev) {
printk(KERN_ERR "Failed to register SPI device\n");
return -ENODEV;
}
// 开始与SPI设备通信,如读写操作前需确保设备已初始化
spi_setup(spi_dev);
// 示例:向SPI设备写入数据
static u8 tx_buf[] = {0x01, 0x02, 0x03};
static u8 rx_buf[3];
spi_message mesg;
spi_transfer t = {
.tx_buf = tx_buf,
.rx_buf = rx_buf,
.len = sizeof(tx_buf),
.speed_hz = spi_dev->max_speed_hz,
.bits_per_word= 8,
};
mesg.transfers = &t;
mesg.num_transfers = 1;
// 发送并接收数据
int status = spi_sync(spi_dev, &mesg);
if (status < 0)
printk(KERN_ERR "Error in spi_sync: %d\n", status);
return 0;
}
static void __exit my_spi_driver_exit(void)
{
spi_unregister_device(spi_dev);
}
module_init(my_spi_driver_init);
module_exit(my_spi_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A Simple SPI Driver Example");
在Arduino或其他AVR单片机上的SPI操作示例(C++):
#include <SPI.h>
// 设置SPI模式、速度等
void setup() {
SPI.begin(); // 初始化SPI接口
SPI.setBitOrder(MSBFIRST); // 设置高位优先(或LSBFIRST设置低位优先)
SPI.setDataMode(SPI_MODE0); // 设置SPI时序模式(根据器件手册选择正确的模式)
SPI.setClockDivider(SPI_CLOCK_DIV4); // 设置SPI时钟速度,这里是CPU频率的1/4
digitalWrite(SS_PIN, HIGH); // 如果使用硬件SS引脚,确保它为高电平以释放从设备
}
void loop() {
// 写入并读取SPI Flash的一个字节
byte address = 0x10; // 假设地址
byte dataToWrite = 0xAA; // 要写入的数据
digitalWrite(SS_PIN, LOW); // 拉低SS信号开始通信
SPI.transfer(address); // 发送地址
SPI.transfer(dataToWrite); // 发送数据
digitalWrite(SS_PIN, HIGH); // 结束通信,拉高SS信号
delayMicroseconds(10); // 等待足够时间让写操作完成(具体延迟取决于器件)
digitalWrite(SS_PIN, LOW); // 再次启动通信以读取数据
SPI.transfer(address | 0x80); // 对于读操作,可能需要设置特定的地址位
byte readData = SPI.transfer(0x00); // 接收数据,同时发送一个不重要的字节
digitalWrite(SS_PIN, HIGH); // 结束通信
Serial.println(readData); // 打印读取到的数据
}