一、概念
定义
SPI(Serial Peripheral Interface,串行外设接口)是一种常用的同步串行全双工通信协议,用于微控制器与各种外设(如传感器、存储器、显示屏等)之间的高速数据交换。
特点:
主从结构:SPI总线由一个主机(Master)控制,主机负责产生时钟(SCK),并通过片选(CS/SS)选择某个从机(Slave)进行通信。
时钟源:只有主机能产生时钟信号,从机没有时钟输出能力,不能主动发起通信。
数据流向:通信时,主机和被选中的从机之间可以双向传输数据(MOSI、MISO),但从机之间没有直接的数据通道。
在SPI通信中主机就好像皇帝,从机就是仆人,只有主机才能选择从机主动发消息,从机只能和主机通信,从机也不能主动和从机通信,只能被动响应“皇帝”的号召
二、SPI结构图
SCKL:
作用:串行时钟线,由主机产生的时钟信号,用于同步数据的发送和接收
说明:没有一个SCKL的时钟周期,MOSI和MISO线上各传输一位数据。数据的采样和发送都依赖于SCKL的时序。
MOSI:
作用:主机输出数据,数据发送数据到从机的信号线
说明:主机通过MOSI线把数据传给被选中的从机
MISO:
作用:从从机发送数据到主机的信号线。
说明:主机通过MISO线从被选中的从机读取数据。比如主机要读传感器数据,就是通过MISO线接收
SSX:
作用:是片选信号,主机用来选择具体的从机(低电平有效)。
说明:每个从机通常有独立的SSX线,主机拉低对应的SSX,选中某个从机进行通信。
三、SPI通信时序图
1.SPI通信基础流程
就是使用SPI通信协议的主从机在通信过程中随时间四个线电平高低的变化。
(1).选择从机:NSS是片选信号,主机(皇帝)翻牌子,选择从机(仆人)进行通信,把电平拉低。
(2).触发就是让时钟SCK开始改变,在变化的过程中,就是向MOSI线输出数据,所以时钟变化是数据开始出入到总线上的标志。.
(3).采样就是让时钟线SCK电平再次变化,然后从机从MOSI线里取出数据,时钟线电平再次恢复到初始电平是从机采集数据的标志。
(4).重复第二步和第三步,直到数据发送完毕,NSS拉回初始电平,这里是高电平,表示输出结束(通信结束)。
2.SPI的四种通信模式
2.1 概念:
四种通信模式就是依靠两个最重要的设置,上面拿出来举例的时序图就是其中一种。依靠时钟极性(CPOL)和时钟相位(CPHA)衍生出四种通信模式。
时钟极性CPOL : 设置时钟空闲时的电平
当CPOL = 0 ,SCK引脚在空闲状态保持低电平;
当CPOL = 1 ,SCK引脚在空闲状态保持高电平。
时钟相位CPHA :设置数据采样时的时钟沿
当 CPHA=0 时,MOSI或 MISO 数据线上的信号将会在 SCK时钟线的奇数边沿被采样
当 CPHA=1时, MOSI或 MISO 数据线上的信号将会在 SCK时钟线的偶数边沿被采样
2.2 四中通信模式:
这个图是手册中的四中时序图,比较简洁,是当CPOL是1还是0选择上面或者下面的时钟线,当CPHA是1还是0的时候选择下面的图为例,最好先把下面简单的理清楚再来消化这个图。
这四个图分开展现更清晰
模式一:
模式二:
模式三:
模式四:
注意:有这四种模式,要求主机和从机处在同一个模式才能正常接收或者发送消息,就好比主机在时钟为一的时候发,从机在时钟为一的时候取,从机能取到数据吗,主机还在发数据,数据还不稳定,很容易出错,明显是不行的。
四、数码管(SPI通信经典案例)
外设的数码管一般是四位:
1.数码管的SPI总线:
2.SPI总线信号的滤波和上拉/上拉电阻电路:
3.四位数码管原理图:
直接看很抽象,现在我把他们连起来,知道他们在整个通信中所处于的位置就好理解了。
4.总结图:
连上线就很明确了第一个SPI连接单片机四个引脚,分别是时钟线,片选线和输入输出线,输入的数据会有一部分传给第二个SPI,因为第二个SPI是控制哪个数码管显示,第一个SPI控制显示的数字。在代码中我们是发送一个有两个数的数组,第一个数组的数据会被挤给第二个SPI,就是第一个SPI不管这个数据直接给第二个SPI,第二个数据是第一个SPI需要处理的,这样就实现对二极管的控制,所以我们发送给这样一个数组只能控制一个灯显示数据。(第二个SPI控制数码管亮灭的连线没有练完,连上不好看就算了,理解的时候不要以为没有)
五、实现数码管的数据显示:
1.配置引脚
选择引脚:
2.程序编写
2.1 显示一个数码管
数码管显示数据的数组:
//定义1个保存了承有数字显示状态的数组,显示:0,1,2,3,4,5,6,7,8,9
const uint8_t number1[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
控制第几个数码管显示的数组:
//定义1个保存了第几个数码管进行显示状态的数组,显示第一个,显示第二个,显示第三个,显示第四个
const uint8_t number2[]={0x01,0x02,0x04,0x08};
将上面添加到特定位置后,在while内编写代码;代码中马上停止输出高电平不影响,因为这是在while循环内,循环速度很快,肉眼捕捉不到闪烁的现象,要是加上长时间的延时才会发生明显闪烁现象。
2.2 显示四个数码管
还可以编写一个函数,只需要在while内修改show_number数值后(数值为十六进制四位数,例如:0x4562显示4562),把show_number当做数据传入数组就可以显示四个数码管。
//定义一个保存了所有数字显示状态的数组,显示:0、1、2、3、4、5、6、7、8、9
const uint8_t number[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
//定义一个用于保存想要显示的数字的变量,volatile使变量不被优化,每次都刷新读取
volatile uint16_t show_number = 0x1234;
//数码管显示函数
void led_dispaly()
{
//创建一个用于保存段选与位选的数组
uint8_t data[2] = {0x00, 0x00}; //位置,数据
//使用switch进行判断,由于一次只能点亮一个,需要引入一个变量进行自增,循环对应各数码管
static uint8_t num = 0; //用于位循环
switch(num)
{
case 0:
data[0] = 0x08; //1-4位顺序为 左0x01 0x02 0x04 0x08右
data[1] = number[show_number & 0x000F]; //将想要显示的数字与之相&,就相当于盖上了其他位
//SPI传输函数,参数为使用的SPI通道、要传输的数据、数据长度、超时时间
HAL_SPI_Transmit(&hspi2,data,2,10);
//进行锁存操作,等效于写入一高一低电平
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET);//片选线
num++; //移动至下一位
break;
case 1:
data[0] = 0x04; //1-4位顺序为 0x01 0x02 0x04 0x08
data[1] = number[show_number>>4 & 0x000F]; // >>4 取第三位
//SPI传输函数,参数为使用的SPI通道、要传输的数据、数据长度、超时时间
HAL_SPI_Transmit(&hspi2,data,2,10);
//进行锁存操作,等效于写入一高一低电平
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET);
num++; //移动至下一位
break;
case 2:
data[0] = 0x02; //1-4位顺序为 0x01 0x02 0x04 0x08
data[1] = number[show_number>>8 & 0x000F];
//SPI传输函数,参数为使用的SPI通道、要传输的数据、数据长度、超时时间
HAL_SPI_Transmit(&hspi2,data,2,10);
//进行锁存操作,等效于写入一高一低电平
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET);
num++; //移动至下一位
break;
case 3:
data[0] = 0x01;//1-4位顺序为 0x01 0x02 0x04 0x08
data[1] = number[show_number>>12 & 0x000F];
//SPI传输函数,参数为使用的SPI通道、要传输的数据、数据长度、超时时间
HAL_SPI_Transmit(&hspi2,data,2,10);
//进行锁存操作,等效于写入一高一低电平
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_RESET);
num = 0; //移动循环
break;
}
}
函数写法不唯一,完全可以自己去写一个属于自己的函数