一、SPI总述
SPI 是一种允许一个主设备启动一个与从设备的同步通讯的协议,从而完成数据的交换。也就是说,SPI是一种规定好的通讯方式。这种通信方式的优点是占用端口较少,一般4根就够基本通讯了。同时传输速度也很高。一般来说要求主设备要有SPI控制器(但可用模拟方式),就可以与基于SPI的芯片通讯了。
常见的SPI外围设备包括FLASHRAM、网络控制器、LCD显示驱动器、A/D转换器和MCU等。
SPI 的通信原理很简单,它需要至少4根线,事实上3根也可以。也是所有基于SPI的设备共有的,它们是SDI(数据输入),SDO(数据输出),SCK(时 钟),CS(片选)。其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效。这就允许在同一总线上连接多个SPI设备成为可能。
接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCK时钟线存在的原 因,由SCK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过SDO线,数据在时钟上沿或下沿时改变,在紧接着的下沿或上沿被读取。 完成一位数据传输,输入也使用同样原理。这样,在至少8次时钟信号的改变(上沿和下沿为一次),就可以完成8位数据的传输。
要注意的是,SCK信号线只由主设备控制,从设备不能控制信号线。同样,在一个基于SPI的设备中,至少有一个主控设备。这就不适用于多处理器的无主控通讯。
这样传输的特点:这样的传输方式有一个优点,与普通的串行通讯不同,普通的串行通讯一次连续传送至少8位数据,而SPI允许数据一位一位的传送,甚至允许暂停,因为SCK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SCK时钟线的控制可以完成对通讯的控制。
SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。
不同的SPI设备的实现方式不尽相同,主要是数据改变和采集的时间不同,在时钟信号上沿或下沿采集有不同定义,需要参考相关器件的文档。
二、SPI的信号线
之前说过,SPI一共有4根信号线,再回顾下作用:
GPIO模拟SPI总的来说是比较简单,把相应的管脚配置成GPIO功能,再按需要配置管脚的输入输出方向,然后根据SPI总线的时序设定IO口的电平。例如,要实现一块LCD的驱动,LCD与主控芯片之间使用SPI协议通信,GPIO就可以这样配置——由于主控芯片不需要从LCD读取数据,SDI可以不接;LCD需要一直被控制,CS接地,使LCD一直处于使能状态。
三、相位和极性
CKPOL (Clock Polarity) = CPOL = POL = Polarity = (时钟)极性
CKPHA (Clock Phase) = CPHA = PHA = Phase = (时钟)相位
1、CPOL 极性
先解释下什么是SCLK时钟的空闲。SCLK空闲就是当SCLK在数发送8个bit比特数据之前和之后的状态,于此对应的,SCLK在发送数据的时候,就是正常的工作的时候,有效active的时刻了。
简单的说,SPI的CPOL,表示当SCLK空闲的时候,其电平的值是低电平0还是高电平。
CPOL=0,时钟空闲idle时候的电平是低电平,所以当SCLK有效的时候,就是高电平,就是所谓的active-high
CPOL=1,时钟空闲idle时候的电平是高电平,所以当SCLK有效的时候,就是低电平,就是所谓的active-low
2、CPHA 相位
相位,对应着数据采样是在第几个边沿(edge),是第一个边沿还是第二个边沿,0对应着第一个边沿,1对应着第二个边沿。
CPHA=0,表示第一个边沿:
对于CPOL=0,idle时候的是低电平,第一个边沿就是从低变到高,所以是上升沿;
对于CPOL=1,idle时候的是高电平,第一个边沿就是从高变到低,所以是下降沿;
CPHA=1,表示第二个边沿:
对于CPOL=0,idle时候的是低电平,第二个边沿就是从高变到低,所以是下降沿;
对于CPOL=1,idle时候的是高电平,第一个边沿就是从低变到高,所以是上升沿;
如上所述,CPOL和CPHA可构成4种组合,这就是常说的SPI四种传输模式——
CPOL=0, CPHA=0
CPOL=0, CPHA=1
CPOL=1, CPHA=0
CPOL=1, CPHA=1
有图有真相:
四、GPIO模拟SPI驱动
概念了解清楚了,我们来上代码吧
#define SS 252 //定义SS所对应的GPIO接口编号 #define SCLK 253 //定义SCLK所对应的GPIO接口编号 #define MOSI 254 //定义SCLK所对应的GPIO接口编号 #define MISO 255 //定义MISO所对应的GPIO接口编号 #define OUTP 1 //表示GPIO接口方向为输出 #define INP 0 //表示GPIO接口方向为输入 /* SPI端口初始化 */ void spi_init() { set_gpio_direction(SS, OUTP); set_gpio_direction(SCLK, OUTP); set_gpio_direction(MOSI, OUTP); set_gpio_direction(MISO, INP); set_gpio_value(SCLK, 0); //CPOL=0 set_gpio_value(MOSI, 0); } /* 从设备使能 enable:为1时,使能信号有效,SS低电平 为0时,使能信号无效,SS高电平 */ void ss_enable(int enable) { if (enable) set_gpio_value(SS, 0); //SS低电平,从设备使能有效 else set_gpio_value(SS, 1); //SS高电平,从设备使能无效 } /* SPI字节写 */ void spi_write_byte(unsigned char b) { int i; for (i=7; i>=0; i--) { set_gpio_value(SCLK, 0); set_gpio_value(MOSI, b&(1<<i)); //从高位7到低位0进行串行写入 delay(); //延时 set_gpio_value(SCLK, 1); // CPHA=1,在时钟的第一个跳变沿采样 delay(); } } /* SPI字节读 */ unsigned char spi_read_byte() { int i; unsigned char r = 0; for (i=0; i<8; i++) { set_gpio_value(SCLK, 0); delay(); //延时 set_gpio_value(SCLK, 1); // CPHA=1,在时钟的第一个跳变沿采样 r = (r <<1) | get_gpio_value(MISO); //从高位7到低位0进行串行读出 delay(); } } /* SPI写操作 buf:写缓冲区 len:写入字节的长度 */ void spi_write (unsigned char* buf, int len) { int i; spi_init(); //初始化GPIO接口 ss_enable(1); //从设备使能有效,通信开始 delay(); //延时 //写入数据 for (i=0; i<len; i++) spi_write_byte(buf[i]); delay(); ss_enable(0); //从设备使能无效,通信结束 } /* SPI读操作 buf:读缓冲区 len:读入字节的长度 */ void spi_read(unsigned char* buf, int len) { int i; spi_init(); //初始化GPIO接口 ss_enable(1); //从设备使能有效,通信开始 delay(); //延时 //读入数据 for (i=0; i<len; i++) buf[i] = spi_read_byte(); delay(); ss_enable(0); //从设备使能无效,通信结束 }