原创 写代码的篮球球痴 嵌入式Linux 2020-06-16
收录于话题
#嵌入式
54个
嵌入式Linux推荐搜索
嵌入式
Linux
程序人生
C语言
最近临近毕业季,应该会有很多人找工作,如果面试的时候,被问到SPI的四种模式是什么,然后你不会,总是会有点尴尬。读了这篇文章,你会对SPI协议有一定的认识,也会对你的面试有帮助。协议是为了规范收发双方的,好的协议不仅需要速度的保证,还需要传输稳定,可拓展等等。
#SPI协议简介
SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器等芯片,还有数字信号处理器和数字信号解码器之间。
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,比如AT91RM9200。
SPI主从模式硬件连接如下图
SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。SPI的四条通讯线是SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)。
MOSI | 主设备数据输出,从设备数据输入 对应MOSI master output slave input |
MISO | 主设备数据输入,从设备数据输出 对应MISO master input slave output |
SCLK | 时钟信号,由主设备产生 |
CS/SS | 从设备使能信号,由主设备控制 |
要注意的是,SCLK信号线只由主设备控制,从设备不能控制信号线。同样,在一个基于SPI的设备中,至少有一个主控设备。
这样传输的特点:
这样的传输方式有一个优点,与普通的串行通讯不同,普通的串行通讯一次连续传送至少8位数据,而SPI允许数据一位一位的传送,甚至允许暂停,因为SCK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据,也就是说,主设备通过对SCK时钟线的控制可以完成对通讯的控制。
SPI还是一个数据交换协议,因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。不同的SPI设备的实现方式不尽相同,主要是数据改变和采集的时间不同,在时钟信号上沿或下沿采集有不同定义。
在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。在多个从设备的系统中,每个从设备需要独立的使能信号,硬件上比I2C系统要稍微复杂一些。
当然了,因为SPI从设备没有地址区分,这样也存在了一个弊端,那就是「没有指定的流控制,没有应答机制确认是否接收到数据」
#SPI总线四种工作方式
SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。
时序详解:
CPOL | 时钟极性选择,为0时SPI总线空闲为低电平,为1时SPI总线空闲为高电平 |
CPHA | 时钟相位选择,为0时在SCK第一个跳变沿采样,为1时在SCK第二个跳变沿采样 |
Mode 0 | SCLK 空闲是为低电平,数据在上升沿有效 |
Mode 1 | SCLK 空闲是为低电平,数据在下降沿有效 |
Mode 2 | SCLK 空闲是为高电平,数据在下降沿有效 |
Mode 3 | SCLK 空闲是为高电平,数据在上升沿有效 |
4种工作模式波形时序如下图:
#协议心得
SPI接口时钟配置心得:
在主设备配置SPI接口时钟的时候一定要弄清楚从设备的时钟要求,因为主设备的时钟极性和相位都是以从设备为基准的。因此在时钟极性的配置上一定要搞清楚从设备是在时钟的上升沿还是下降沿接收数据,是在时钟的下降沿还是上升沿输出数据。
但要注意的是,主设备的SDO连接从设备的SDI,从设备的SDO连接主设备的SDI。
从设备SDI接收的数据是主设备的SDO发送过来的,主设备SDI接收的数据是从设备SDO发送过来的,所以主设备这边SPI时钟极性的配置(即SDO的配置)跟从设备的SDI接收数据的极性是相反的,跟从设备SDO发送数据的极性是相同的。下面这段话是Sychip Wlan8100 Module Spec上说的,充分说明了时钟极性是如何配置的:
The 81xx module will always input data bits at the rising edge of the clock, and the host will always output data bits on the falling edge of the clock.
意思是:
主设备在时钟的下降沿发送数据,从设备在时钟的上升沿接收数据。因此主设备这边SPI时钟极性应该配置为下降沿有效。
又如,下面这段话是摘自LCD Driver IC SSD1289:
SDI is shifted into 8-bit shift register on every rising edge of SCK in the order of data bit 7, data bit 6 …… data bit 0.
意思是:
从设备SSD1289在时钟的上升沿接收数据,而且是按照从高位到地位的顺序接收数据的。因此主设备的SPI时钟极性同样应该配置为下降沿有效。
时钟极性和相位配置正确后,数据才能够被准确的发送和接收, 因此应该对照从设备的SPI接口时序或者Spec文档说明来正确配置主设备的时钟。
#SPI协议软件模拟
单片机软件模拟SPI接口—加深理解SPI总线协议
现以 AT89C205l单片机模拟SPI总线操作串行EEPROM 93CA6为例,如图1所示,介绍利用单片机的I/O口通过软件模拟SPI总线的实现方法。在这里,仅介绍读命令的时序和应用子程序。
93C46存储器SPI总线的工作原理
93CA6作为从设备,其SPI接口使用4条I/O口线:串行时钟线(SK)、输出数据线DO、输入数据线DI和高电平有效的从机选择线CS。其数据的传输格式是高位(MSB)在前,低位(LsB)在后。93C46的SPI总线接口读命令时序如图2所示。
软件模拟SPI接口的实现方法
对于不带SPI串行总线接口的AT89C2051单片 机来说,可以使用软件来模拟SPI的操作,图1所示 为AT89C2051单片机与串行EEPROM 93C46的硬件 连接图,其中,P1.0模拟SPI主设备的数据输出端 SDO,P1.2模拟SPI的时钟输出端SCK,P1.3模拟 SPI的从机选择端SCS,P1.1模拟SPI的数据输入 SDI。
上电复位后首先先将P1.2(SCK)的初始状态设置为0(空闲状态)。
读操作:
AT89C2051首先通过P1.0口发送1位起始位(1),2位操作码(10),6位被读的数据地址(A5A4A3A2A1A0),然后通过P1.1口读1位空位(0),之后再读l6位数据(高位在前)。
写操作:
AT89C2051首先通过P1.0口发送1位起始位(1),2位操作码(01),6位被写的数据地址(A5A4A3A2A1A0),之后通过P1.0口发送被写的l6位数据(高位在前),写操作之前要发送写允许命令,写之后要发送写禁止命令。
写允许操作(WEN)):
写操作首先发送1位起始位(1),2位操作码(00),6位数据(11XXXX)。
写禁止操作(WDS)):
写操作首先发送1位起始位(1),2位操作码(00),6位数据(00XXXX)。
下面介绍用C51模拟SPI的子程序。
/*例子1*/
//首先定义好I/O口
sbit SDO=P1^0;
sbit SDI=P1^1;
sbit SCK=P1^ 2;
sbit SCS=P1^3;
sbit ACC_7= ACC^7;
unsigned int SpiRead(unsigned char add)
{
unsigned char i;
unsigned int datal6;
add&=0x3f;/*6位地址*/
add |=0x80;/*读操作码l0*/
SDO=1;/*发送1为起始位*/
SCK=0;
SCK=1;
for(i=0;<8;i++)/*发送操作码和地址*/
{
if(add&0x80==1)
SDO=1;
else
SDO=0;
SCK=0;/*从设备上升沿接收数据*/
SCK=1;
add<<= 1;
}
SCK=1;/*从设备时钟线下降沿后发送数据,空读1位数据*/
SCK=0;
datal6<<= 1;/*读16位数据*/
for(i=0;<16;i++)
{
SCK= 1;
_nop_();
if(SDI==1)
datal6|=0x01;
SCK =0;
datal6< < =1;
}
return datal6;
}
/*例子2*/
#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);//从设备使能无效,通信结束
}
对于不同的串行接口外围芯片,它们的时钟时序是不同的。上述子程序是针对在SCK的上升沿输入(接收)数据和在下降沿输出(发送)数据的器件。这些子程序也适用于在串行时钟)的上升沿输入和下降沿输出的其它各种串行外围接口芯片,只要在程序中改变P1.2(SCK)的输出电平顺序进行相应调整即可。
示波器抓取的SPI波形
#Linux下的SPI源码
链接:
https://pan.baidu.com/s/1Jm_gDxj-to965ZH3I16dug
密码:c9ki
推荐阅读:专辑|Linux文章汇总专辑|程序人生