以下内容主要参考于维基百科。,其中的实验主要基于正点原子的
S
T
M
32
F
103
Z
E
T
6
STM32F103ZET6
STM32F103ZET6的精英版开发板。
S
P
I
(
S
e
r
i
a
l
P
e
r
i
p
h
e
r
a
l
I
n
t
e
r
f
a
c
e
)
SPI(Serial\quad Peripheral\quad Interface)
SPI(SerialPeripheralInterface)协议是一种同步串行通信协议,它主要用于嵌入式系统中的短距离通信。它由摩托罗拉公司在
1980
1980
1980年代中期设计并且现在已经成为了事实上的标准。最常见的
S
P
I
SPI
SPI 协议需要四根线进行通信,好像还有变种的
S
P
I
SPI
SPI 协议。
- 片选线(CS,chip select):从主机输出,一般低电平有效。
- 主机输出从机输入线(MOSI:Master Output Slave In ):从主机输出。
- 主机输入从机输出线(MISO:Master Input Slave Output ):从主机输入。
- 时钟信号线(CLK,clock):从主机输出。
S P I SPI SPI 协议通信采用主从机的模式,只能有一个主机,可以有多个从机。因为有两根数据线因此可以全双工通信。通信的发起由主机进行。主机输出时钟信号然后将与之要通信的从机的片选信号线拉低就可以通信了。这里没有 I 2 C I2C I2C协议中每8个比特的固定数据帧格式(每8个比特就需要一个 A C K ACK ACK信号),可以发送任意比特的数据,如果不想通信了,主机将片选信号线拉高就可以停止通信了。一个典型的 S P I SPI SPI 协议通信的连接图如图1所示。图1来至于这里。
接下来讲一下 S P I SPI SPI 协议配置中的时钟极性和相位的定义:
- 时钟极性
C
P
O
L
CPOL
CPOL指的是在
S
P
I
SPI
SPI 协议总线中没有数据通信时时钟信号线的高低电平状态:
- 值为0:没有数据通信时时钟信号线为低电平状态
- 值为1:没有数据通信时时钟信号线为高电平状态
- 时钟相位
C
P
H
A
CPHA
CPHA指的是在
S
P
I
SPI
SPI 协议总线数据通信时信号采样的时间点:
- 值为0:在每一个时钟信号周期的第一个电平边缘采样
- 值为1:在每一个时钟信号周期的第二个电平边缘采样
时钟极性非常好理解,重点结合图2讲一下时钟相位,图2来至于这里。。图2中从左到右的两条绿线分别表示通信的开始与结束,也分别对应着片选信号线的拉低与拉高。两条蓝色的线表示一个完整的时钟周期,左边的绿线和第一条蓝色的线表示第一个时钟周期,最后一条蓝线和最右边的绿线只有半个时钟周期。红色的线表示第一个电平边缘,蓝色的线表示第二个电平边缘。如果这样来看的话,在时钟相位为0的时候一个时钟信号的周期内也对应一个比特位对应的周期,但是在时钟相位为1的时候一个比特位对应的周期跨越了两个时钟信号周期。按照时钟信号极性与相位的组合, S P I SPI SPI 协议一共有四种模式。
下面讲一下 S P I SPI SPI 协议的主设备和从设备常见的两种连接方式。第一种是一个主机和多个相互独立的从机,第二种是 D a i s y − c h a i n e d Daisy-chained Daisy−chained模式的一个主机和多个相互合作的从机。分别如图3和图4所示。第一种模式下每一个从设备有一根单独的片选线,每次当要和特定的从设备通信时将将该设备的片选线拉低就可以了。当然同一时刻只能有一个设备的片选线被拉低,不能同时有多根片选线被拉低。这里一般会建议每一根片选线都接一个独立的上拉电阻以避免干扰,然后 M I S O MISO MISO引脚是三态的(高,低和高阻态(就相当于断路))。当某个从设备没有通信时,它的 M I S O MISO MISO引脚应该是高阻态。第二种模式下所有从设备的片选线都连接到主机的唯一的同一根片选线上,所有从设备通过 M I S O MISO MISO引脚和 M O S I MOSI MOSI引脚串联在一起,前一个从设备的 M I S O MISO MISO引脚连接到下一个从设备的 M O S I MOSI MOSI引脚。主设备的 M O S I MOSI MOSI引脚连接到第一个从设备的 M O S I MOSI MOSI引脚,最后一个从设备的 M I S O MISO MISO引脚连接到主机的 M I S O MISO MISO引脚。
下面讲一下 S P I SPI SPI 协议的具体通信过程,如图5所示。主机通过输出时钟信号并拉低片选线来开始通信,在全双工模式下。在每一个时钟周期,主机通过 M O S I MOSI MOSI信号线发送一个比特位,从机通过 M O S I MOSI MOSI信号线接受该比特位。与此同时从机通过 M I S O MISO MISO信号线发送一个比特位,主机通过 M I S O MISO MISO信号线接受该比特位。前面提到 S P I SPI SPI 协议可以发送任意比特的数据,如果不想通信了,主机将片选信号线拉高就可以停止通信了。但是通常情况下通信还是以8比特或16比特为单位进行通信。所以一般在主机和从机中各有一个8比特或16比特的移位寄存器,在通信过程中它们组成了一个虚拟的环状结构,在每一个时钟周期,主机通过 M O S I MOSI MOSI信号线发送一个比特位,其实就是主机的移位寄存器中8比特数据的最高位(最高位或最低位移出都是可以的,这里我们假设移出最高位且图5中8个元素的数组的最右位为最高位)移出移位寄存器到 M O S I MOSI MOSI信号线上,主机的移位寄存器中剩余的低7位也会各移一位,因此此时主机的移位寄存器的最低位也就是最左边的位会空出来,从机通过 M O S I MOSI MOSI信号线接受该比特位并存放到从机的移位寄存器的最低位,与此同时从机通过 M I S O MISO MISO信号线发送一个比特位,其实就是从机的移位寄存器中8比特数据的最高位移出移位寄存器到 M I S O MISO MISO信号线上,从机的移位寄存器中剩余的低7位也会各右移一位,因此此时从机的移位寄存器的最低位也就是最左边的位会空出来(其实这里不会空出来,而是刚刚从主机接收的那个比特位放到了这里),主机通过 M I S O MISO MISO信号线接收该比特位并存放到主机的移位寄存器的最低位,也就是刚刚空出来的哪一位。重复以上操作8次,这样主设备和从设备就各自发送和接收了一个字节的数据。一般 S P I SPI SPI 模块会有两个数据缓冲器,发送缓冲器和接收缓冲器,发送数据的时候直接把要发送的8比特或16比特数据放入发送缓冲器里面就可以了,接下来开始发送之后,这8比特或16比特数据或并行的一次性放入8比特或16比特移位寄存器,具体的发送过程就是我们刚才提到的移位寄存器不断移位的过程。当完成8比特或16比特的接收之后会将这8比特或16比特数据放入对应的接收缓冲器中。
下面再结合正点原子的 S P I SPI SPI读写 F L A S H FLASH FLASH的实验来对 S P I SPI SPI通信有一个实际的了解。这里只讲一下工程里面的 s p i . c spi.c spi.c文件和 f l a s h . c flash.c flash.c文件这两个文件,因为只有这两个文件是和 S P I SPI SPI操作相关的。我们先来看一下 s p i . c spi.c spi.c文件,如下代码段所示,这里使用的是 S P I 2 SPI2 SPI2。
/*spi.c*/
#include "spi.h"
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
RCC_APB1PeriphClockCmd( RCC_APB1Periph_SPI2, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Cmd(SPI2, ENABLE);
SPI2_ReadWriteByte(0xff);
}
u8 SPI2_ReadWriteByte(u8 TxData)
{
u8 retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET)
{
retry++;
if(retry>200)
{
return 0;
}
}
SPI_I2S_SendData(SPI2, TxData);
retry=0;
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET)
{
retry++;
if(retry>200)
{
return 0;
}
}
return SPI_I2S_ReceiveData(SPI2);
}
S
P
I
2
_
I
n
i
t
SPI2\_Init
SPI2_Init初始化函数对
S
P
I
2
SPI2
SPI2的
M
O
S
I
MOSI
MOSI、
M
I
S
O
MISO
MISO以及
S
C
L
K
SCLK
SCLK三根信号线对应的
G
P
I
O
GPIO
GPIO引脚进行了初始化,具体的配置参数可参考图6和图7。图6和图7来自于对应芯片的数据手册和参考手册。其中片选引脚的配置见于
s
p
i
.
c
spi.c
spi.c文件的以下语句以及
f
l
a
s
h
.
c
flash.c
flash.c文件中。对于该选项的配置开始的时候还是很迷惑的,自从看了这位哥的解释才清楚了。
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
以上语句配置主机的片选引脚为软件控制方式,也就是置
C
R
1
CR1
CR1寄存器的第九个比特位
S
S
M
SSM
SSM为1,如图8所示。那么什么是软件控制方式,我们可以结合图9来看。其实可以这样认为
S
T
M
32
STM32
STM32芯片内部
S
P
I
SPI
SPI模块有各自的
M
O
S
I
MOSI
MOSI、
M
I
S
O
MISO
MISO、
S
C
L
K
SCLK
SCLK以及
N
S
S
NSS
NSS引脚( 模块自身引脚),其中
M
O
S
I
MOSI
MOSI、
M
I
S
O
MISO
MISO、
S
C
L
K
SCLK
SCLK以及
N
S
S
NSS
NSS这些模块自身引脚有复用功能下有对应的
G
P
I
O
GPIO
GPIO口( 模块外部引脚)。
M
O
S
I
MOSI
MOSI、
M
I
S
O
MISO
MISO、
S
C
L
K
SCLK
SCLK这三个模块自身引脚都是直接连接到其复用功能下对应的
G
P
I
O
GPIO
GPIO口的。
N
S
S
NSS
NSS引脚比较特殊,
N
S
S
NSS
NSS引脚在配置为软件控制时,也就是置
C
R
1
CR1
CR1寄存器的第九个比特位
S
S
M
SSM
SSM为1时,其模块自身引脚和模块外部引脚是完全断开的,此时模块外部引脚可以作为独立的
G
P
I
O
GPIO
GPIO引脚且和
S
P
I
SPI
SPI模块没有任何关系。
N
S
S
NSS
NSS引脚在配置为硬件控制时,也就是置
C
R
1
CR1
CR1寄存器的第九个比特位
S
S
M
SSM
SSM为0时,其模块自身引脚和模块外部引脚是连接在一起的。以上对主机模式和从机模式都适用。
接下来我们从主机和从机的角度上再来讨论一下各种情况下:
- 主机模式:
- 片选引脚软件控制:这时一般是不准备使用模块片选引脚分配的
G
P
I
O
GPIO
GPIO口,该
G
P
I
O
GPIO
GPIO口可以用作它用,此时的情况可能是主机需要多根片选线来与多个从设备通信,因此需要多个普通的
G
P
I
O
GPIO
GPIO口来作为片选线。
C
R
2
CR2
CR2寄存器的第二个比特位
S
S
O
E
SSOE
SSOE决定该模块自身引脚输出是否使能,如图10所示,如果该比特位的值为1,则输出是使能的,如果该比特位的值为0,那就是不使能的,也就是输入。
C
R
1
CR1
CR1寄存器的第八个比特位
S
S
I
SSI
SSI此时是连接到模块自身
N
S
S
NSS
NSS引脚的,该位的状态决定了
S
P
I
SPI
SPI模块的内部
N
S
S
NSS
NSS模块引脚的输入和输出状态。
- 片选引脚输出不使能:一般在主机的软件管理模式下模块自身引脚输出是不使能的并且 S S I SSI SSI位的值1,相当于模块自身引脚输入高电平,这样做是为了防止 S P I SPI SPI模块进入 M a s t e r M o d e F a u l t Master\quad Mode\quad Fault MasterModeFault状态而导致 S P I SPI SPI模块失效。
- 片选引脚输出使能:就算这里输出使能,因为模块自身引脚和模块外部引脚是完全断开的,输出的也没地方去,但是至于这里能不能通过使能自身引脚输出( S S I SSI SSI位的值不关心)来达到同样的防止 S P I SPI SPI模块进入 M a s t e r M o d e F a u l t Master\quad Mode\quad Fault MasterModeFault状态的效果我这里也不清楚。
- 片选引脚硬件控制:此时
S
S
I
SSI
SSI位的值可以忽略。
- 片选引脚输出不使能:此时也是不准备使用模块片选引脚分配的 G P I O GPIO GPIO口,但是此时模块自身引脚和模块外部引脚是连接到一起的,防止 S P I SPI SPI模块进入 M a s t e r M o d e F a u l t Master\quad Mode\quad Fault MasterModeFault状态,模块外部引脚必须接上高电平。这时 S P I SPI SPI模块可以通过选用其它的普通 G P I O GPIO GPIO口来作为 N S S NSS NSS引脚来指定和那个从设备进行通信,这里也相当于是达到了可以选择多个从设备的能力。
- 片选引脚输出使能:在这种配置之下 S P I SPI SPI模块开启之后 N S S NSS NSS引脚一直处于低状态直到关闭 S P I SPI SPI模块。这里就没有了拉高关闭通信和拉低开始通信的过程了。这里如果你试图通过 G P I O GPIO GPIO的接口函数对 N S S NSS NSS引脚拉高或拉低无效。
- 片选引脚软件控制:这时一般是不准备使用模块片选引脚分配的
G
P
I
O
GPIO
GPIO口,该
G
P
I
O
GPIO
GPIO口可以用作它用,此时的情况可能是主机需要多根片选线来与多个从设备通信,因此需要多个普通的
G
P
I
O
GPIO
GPIO口来作为片选线。
C
R
2
CR2
CR2寄存器的第二个比特位
S
S
O
E
SSOE
SSOE决定该模块自身引脚输出是否使能,如图10所示,如果该比特位的值为1,则输出是使能的,如果该比特位的值为0,那就是不使能的,也就是输入。
C
R
1
CR1
CR1寄存器的第八个比特位
S
S
I
SSI
SSI此时是连接到模块自身
N
S
S
NSS
NSS引脚的,该位的状态决定了
S
P
I
SPI
SPI模块的内部
N
S
S
NSS
NSS模块引脚的输入和输出状态。
- 从机模式:从机模式时片选引脚应该是输入模式,
S
S
O
E
SSOE
SSOE位这里应该是不适用的,默认为0就好。
- 片选引脚软件控制:此时如果从机要和主机进行通信, S S I SSI SSI位要置0,相当于拉低,如果为1的话将无法通信。
- 片选引脚硬件控制:此时当片选模块外部引脚检测到低电平时就可以和主机进行通信了。
我们这里设置的是主机模式的片选信号的软件管理方式,从标准库的 S P I SPI SPI头文件可以看出(图11)主模式时自动将 C R 1 CR1 CR1寄存器的第八个比特位为1,这是为了保证在片选信号由软件来进行管理的情况下 S P I SPI SPI模块的内部 N S S NSS NSS引脚为高电平而防止 S P I SPI SPI模块进入 M a s t e r M o d e F a u l t Master\quad Mode\quad Fault MasterModeFault状态而导致 S P I SPI SPI模块失效。
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
这里有一个地方要注意,数据手册和参考手册里面提到的
N
S
S
NSS
NSS引脚都是指的模块内部的引脚而不是复用功能对应的
G
P
I
O
GPIO
GPIO引脚,只要在这个概念下你去看手册时才不会懵。
当
S
P
I
2
_
I
n
i
t
SPI2\_Init
SPI2_Init初始化函数中其它参数的配置都一目了然,这里就不多说了。
接下来来看一下
f
l
a
s
h
.
c
flash.c
flash.c文件,但是首先还是介绍一下正点原子的精英板上用于
S
P
I
SPI
SPI实验的
F
L
A
S
H
FLASH
FLASH,目前正点原子的实验文档上写的它的型号为
W
i
n
B
o
n
d
W
25
Q
128
F
V
S
P
I
f
l
a
s
h
WinBond\quad W25Q128FV\quad SPI\quad flash
WinBondW25Q128FVSPIflash,但是最新的板子已经换成苏州诺存微电子有限公司的
N
M
2
Q
128
NM2Q128
NM2Q128
F
L
A
S
H
FLASH
FLASH,这两个
F
L
A
S
H
FLASH
FLASH基本操作都一样,只是
M
a
n
u
f
a
c
t
u
r
e
I
D
Manufacture ID
ManufactureID不一样,现在的
M
a
n
u
f
a
c
t
u
r
e
I
D
Manufacture ID
ManufactureID为
0
x
52
0x52
0x52,所以在正点原子的源代码中得改一下。它的容量大小为
128
M
b
i
t
=
16
M
b
y
t
e
128M\ bit=16M\ byte
128M bit=16M byte。它支持标准的
S
P
I
SPI
SPI协议以及类
S
P
I
SPI
SPI协议。它一共分为
65536
65536
65536个页,每个页的大小为
256
B
y
t
e
256Byte
256Byte,每次最多可以写入
256
B
y
t
e
256Byte
256Byte,也就是一页。它也可以分为
4096
4096
4096个扇区,每个扇区
4
K
B
4KB
4KB。它也可以分为
256
256
256个块,每个块
64
K
B
64KB
64KB。每次擦除的单位可以是扇区,块和半个块。注意以下关于
f
l
a
s
h
flash
flash的命令的相关截图都是从
W
i
n
B
o
n
d
W
25
Q
128
F
V
S
P
I
f
l
a
s
h
WinBond\quad W25Q128FV\quad SPI\quad flash
WinBondW25Q128FVSPIflash的参考手册截取的,虽然实际的芯片是州诺存微电子有限公司的,但是这也不影响,功能基本一样。
/*flash.c*/
#include "flash.h"
#include "spi.h"
#include "delay.h"
#include "usart.h"
u8 W25QXX_BUFFER[4096];
u16 W25QXX_TYPE=0;
void W25QXX_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
W25QXX_CS(Bit_SET);
SPI2_Init();
W25QXX_TYPE=W25QXX_ReadID();
}
u8 W25QXX_ReadSR(void)
{
u8 byte=0;
W25QXX_CS(Bit_RESET);
SPI2_ReadWriteByte(W25X_ReadStatusReg);
byte=SPI2_ReadWriteByte(0Xff);
W25QXX_CS(Bit_SET);
return byte;
}
void W25QXX_Write_Enable(void)
{
W25QXX_CS(Bit_RESET);
SPI2_ReadWriteByte(W25X_WriteEnable);
W25QXX_CS(Bit_SET);
}
u16 W25QXX_ReadID(void)
{
u16 Temp = 0;
W25QXX_CS(Bit_RESET);
SPI2_ReadWriteByte(0x90);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
SPI2_ReadWriteByte(0x00);
Temp|=SPI2_ReadWriteByte(0xFF)<<8;
Temp|=SPI2_ReadWriteByte(0xFF);
W25QXX_CS(Bit_SET);
return Temp;
}
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)
{
u16 i;
W25QXX_CS(Bit_RESET);
SPI2_ReadWriteByte(W25X_ReadData);
SPI2_ReadWriteByte((u8)((ReadAddr)>>16));
SPI2_ReadWriteByte((u8)((ReadAddr)>>8));
SPI2_ReadWriteByte((u8)ReadAddr);
for(i=0;i<NumByteToRead;i++)
{
pBuffer[i]=SPI2_ReadWriteByte(0XFF);
}
W25QXX_CS(Bit_SET);
}
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 i;
W25QXX_Write_Enable();
W25QXX_CS(Bit_RESET);
SPI2_ReadWriteByte(W25X_PageProgram);
SPI2_ReadWriteByte((u8)((WriteAddr)>>16));
SPI2_ReadWriteByte((u8)((WriteAddr)>>8));
SPI2_ReadWriteByte((u8)WriteAddr);
for(i=0;i<NumByteToWrite;i++)
{
SPI2_ReadWriteByte(pBuffer[i]);
}
W25QXX_CS(Bit_SET);
W25QXX_Wait_Busy();
}
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u16 pageremain;
pageremain=256-WriteAddr%256;
if(NumByteToWrite<=pageremain)
{
pageremain=NumByteToWrite;
}
while(1)
{
W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
if(NumByteToWrite==pageremain)
{
break;
}
else
{
pBuffer+=pageremain;
WriteAddr+=pageremain;
NumByteToWrite-=pageremain;
if(NumByteToWrite>256)
{
pageremain=256;
}
else
{
pageremain=NumByteToWrite;
}
}
};
}
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
u32 secpos;
u16 secoff;
u16 secremain;
u16 i;
u8 * W25QXX_BUF;
W25QXX_BUF=W25QXX_BUFFER;
secpos=WriteAddr/4096;
secoff=WriteAddr%4096;
secremain=4096-secoff;
if(NumByteToWrite<=secremain)
{
secremain=NumByteToWrite;
}
while(1)
{
W25QXX_Read(W25QXX_BUF,secpos*4096,4096);
for(i=0;i<secremain;i++)
{
if(W25QXX_BUF[secoff+i]!=0XFF)
{
break;
}
}
if(i<secremain)
{
W25QXX_Erase_Sector(secpos);
for(i=0;i<secremain;i++)
{
W25QXX_BUF[i+secoff]=pBuffer[i];
}
W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);
}else
{
W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
}
if(NumByteToWrite==secremain)
{
break;
}
else
{
secpos++;
secoff=0;
pBuffer+=secremain;
WriteAddr+=secremain;
NumByteToWrite-=secremain;
if(NumByteToWrite>4096)
{
secremain=4096;
}
else
{
secremain=NumByteToWrite;
}
}
};
}
void W25QXX_Erase_Sector(u32 Dst_Addr)
{
Dst_Addr*=4096;
W25QXX_Write_Enable(); //SET WEL
W25QXX_Wait_Busy();
W25QXX_CS(Bit_RESET);
SPI2_ReadWriteByte(W25X_SectorErase);
SPI2_ReadWriteByte((u8)((Dst_Addr)>>16));
SPI2_ReadWriteByte((u8)((Dst_Addr)>>8));
SPI2_ReadWriteByte((u8)Dst_Addr);
W25QXX_CS(Bit_SET);
W25QXX_Wait_Busy();
}
void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR()&0x01)==0x01);
}
首先介绍初始化函数
W
25
Q
X
X
_
I
n
i
t
W25QXX\_Init
W25QXX_Init,因为之前在
S
P
I
2
SPI2
SPI2初始化的时候,
S
P
I
2
SPI2
SPI2模块的
N
S
S
NSS
NSS引脚配置为软件管理因此需要额外的一根
G
P
I
O
GPIO
GPIO线用来做片选的信号线,这里刚好就是配置这根片选线。
S
P
I
2
SPI2
SPI2模块的
N
S
S
NSS
NSS引脚配置为软件管理因此它原来的那根复用功能对应的
G
P
I
O
GPIO
GPIO线就可以作为普通的
G
P
I
O
GPIO
GPIO而不再作为
S
P
I
2
SPI2
SPI2模块固定的那根片选线,因此这里就是把原本在硬件模式下固定的片选线作为目前的额外的片选线。然后就是拉高片选信号以及
S
P
I
2
SPI2
SPI2模块的初始化,最后读
f
l
a
s
h
flash
flash的
M
a
n
u
f
a
c
u
r
e
/
D
e
v
i
c
e
I
D
Manufacure/Device\quad ID
Manufacure/DeviceID。
接下来就讲一下读
f
l
a
s
h
flash
flash的
M
a
n
u
f
a
c
u
r
e
/
D
e
v
i
c
e
I
D
Manufacure/Device\quad ID
Manufacure/DeviceID函数
W
25
Q
X
X
_
R
e
a
d
I
D
W25QXX\_ReadID
W25QXX_ReadID。我们结合
f
l
a
s
h
flash
flash的数据手册命令章节的相关内容来看,如图12所示。结合数据手册来看就比较简单了,先拉低片选信号线选中设备,然后发送命令字节数据
90
90
90,接着发送三个字节数据
00
00
00,接着读出来的两个字节就是了。
接着讲一下
W
25
Q
X
X
_
W
r
i
t
e
W25QXX\_Write
W25QXX_Write函数,该函数在
f
l
a
s
h
flash
flash的指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr开始写入指定个数
N
u
m
B
y
t
e
T
o
W
r
i
t
e
NumByteToWrite
NumByteToWrite的字节数据。我们还是结合图13的数据手册相应的命令说明来看。因为该型号的
f
l
a
s
h
flash
flash规定如果要在相应的地址写的话,该地址的数据应该首先被擦除(擦除后的数据为
0
x
F
F
0xFF
0xFF),且擦除的最小单位为一个扇区(4096字节)。
s
e
c
p
o
s
secpos
secpos为指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr所在的扇区的编号(第一个扇区编号为0,最后一个扇区编号为65535),
s
e
c
o
f
f
secoff
secoff为指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr在所在的扇区的地址偏移(
0
−
>
4095
0->4095
0−>4095),
s
e
c
r
e
m
a
i
n
secremain
secremain为从指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr开始算起到其所在的扇区的末尾总共的字节数。如果
s
e
c
r
e
m
a
i
n
secremain
secremain小于等于
N
u
m
B
y
t
e
T
o
W
r
i
t
e
NumByteToWrite
NumByteToWrite则
N
u
m
B
y
t
e
T
o
W
r
i
t
e
NumByteToWrite
NumByteToWrite赋值给
s
e
c
r
e
m
a
i
n
secremain
secremain。程序先检测从指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr开始到当前扇区的最后一个字节是否有不等于
0
x
F
F
0xFF
0xFF的字节数据,如果有,先将指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr所在的扇区的所有数据读出来存放在数组
W
25
Q
X
X
_
B
U
F
W25QXX\_BUF
W25QXX_BUF中,然后将数组
p
B
u
f
f
e
r
pBuffer
pBuffer中从索引0开始的
s
e
c
r
e
m
a
i
n
secremain
secremain个要写入的数据复制到数组
W
25
Q
X
X
_
B
U
F
W25QXX\_BUF
W25QXX_BUF中,这样是为了保持指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr所在的扇区的第0个字节到指定地址的前一个地址的数据不被改变。擦除该扇区,并将组
W
25
Q
X
X
_
B
U
F
W25QXX\_BUF
W25QXX_BUF中的4096个字节数据写入到刚刚擦除的扇区中。如果没有,就没有必要擦除了,直接从指定地址
W
r
i
t
e
A
d
d
r
WriteAddr
WriteAddr开始将数组
p
B
u
f
f
e
r
pBuffer
pBuffer中从索引0开始的
s
e
c
r
e
m
a
i
n
secremain
secremain个要写入的数据写入到当前扇区中。如果
N
u
m
B
y
t
e
T
o
W
r
i
t
e
=
s
e
c
r
e
m
a
i
n
NumByteToWrite=secremain
NumByteToWrite=secremain整个写入过程就结束了,如果没有则就要在下一个扇区中重复以上过程来写入剩下的数据。
其它函数就比较简单了,参照
f
a
l
s
h
falsh
falsh的数据手册的相应命令章节可以一目了然,这里就不多讲了。
最后看一下 m a i n . c main.c main.c函数。最开始检测 f a l s h falsh falsh的型号是否是 W 25 Q 128 W25Q128 W25Q128如果不是则报错,如果是的话,按下按键 K E Y 1 KEY1 KEY1会在特定的地址写入特定的字节数据:,按下按键 K E Y 0 KEY0 KEY0会在刚才的写入地址读取刚才写入的数据。具体工程在这里。
/*main.c*/
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include "flash.h"
const u8 TEXT_Buffer[]={"ELITE STM32 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)
int main(void)
{
u8 key;
u16 i=0;
u8 datatemp[SIZE];
u32 FLASH_SIZE;
delay_init();
uart_init(USART1,115200);
LED_Init();
KEY_Init();
W25QXX_Init();
printf("KEY1:Write, KEY0:Read.\r\n");
while(W25QXX_ReadID()!=W25Q128)
{
printf("W25Q128 Check Failed!\r\n");
delay_ms(500);
printf("Please Check!\r\n");
delay_ms(500);
GPIO_WriteBit(GPIOB, GPIO_Pin_5, !GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5));
}
printf("W25Q128 Ready!\r\n");
FLASH_SIZE=16*1024*1024;
while(1)
{
key=KEY_Scan(0);
if(key==KEY1_PRES)
{
printf("Start Write W25Q128....\r\n");
W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);
printf("W25Q128 Write Finished!\r\n");
}
if(key==KEY0_PRES)
{
printf("Start Read W25Q128.... \r\n");
W25QXX_Read(datatemp,FLASH_SIZE-100,SIZE);
printf("The Data Readed Is:%s\r\n",datatemp);
}
i++;
delay_ms(10);
if(i==20)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_5, !GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5));
i=0;
}
}
}
图13为测试结果。
接下来我们来跑一下两块正点原子 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM32F103ZET6精英板的 S P I SPI SPI全双工主从机通信测试。主机向从机发送10个字节的数据,同时接收从从机发送来的10个字节的数据,从机向主机发送10个字节的数据,同时接收从主机发送来的10个字节的数据。主从机都使用 S P I 1 SPI1 SPI1,片选引脚都使用硬件管理方式,也就是主机这边拉低片选引脚后才能与从机通信,主机这边要使能片选引脚输出。我这里主要给出了主机和从机 S P I SPI SPI初始化和主函数的核心代码,分别如下所示:
/*spi.c(MASTER)*/
#include "spi.h"
void My_Spi_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1, ENABLE );
/*GPIO_Pin_6 is MISO signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*GPIO_Pin_7 is MOSI signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*GPIO_Pin_5 is SCK signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*GPIO_Pin_4 is slected as hardware controlled CS signal(NSS output enabled).*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
/*Enable the NSS output*/
SPI_SSOutputCmd(SPI1,ENABLE);
GPIO_WriteBit(GPIOA,GPIO_Pin_4,Bit_SET);
}
u8 Spi_Read_Write_Byte(u8 TxData)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, TxData);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1);
}
void My_Spi_Read_Write(u8* TxBuffer,u8* RxBuffer,u16 NumOfBytes)
{
u16 i;
GPIO_WriteBit(GPIOA,GPIO_Pin_4,Bit_RESET);
for(i=0;i<NumOfBytes;i++)
{
RxBuffer[i]=Spi_Read_Write_Byte(TxBuffer[i]);
}
GPIO_WriteBit(GPIOA,GPIO_Pin_4,Bit_SET);
}
/*main.c(MASTER)*/
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include "spi.h"
u8 sendData[10]={5,6,7,8,9,10,11,12,13,14};
u8 receivedData[10]={0,0,0,0,0,0,0,0,0,0};
int main(void)
{
u8 key=0;
u8 i=0;
delay_init();
uart_init(USART1,115200);
LED_Init();
KEY_Init();
My_Spi_Init();
printf("SPI full-duplex communication test start. This is master.\r\n");
while(1)
{
key=KEY_Scan(0);
if(key==KEY0_PRES)
{
printf("KEY0 is pressed.\r\n");
My_Spi_Read_Write(sendData,receivedData,10);
printf("The received data is:.\r\n");
for(i=0;i<10;i++)
{
printf("%d.\r\n",receivedData[i]);
}
key=0;
}
else
{
/*LED twinkle*/
GPIO_WriteBit(GPIOB, GPIO_Pin_5, !GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5));
delay_ms(200);
}
}
}
/*spi.c(SLAVE)*/
#include "spi.h"
void My_Spi_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA|RCC_APB2Periph_SPI1, ENABLE );
/*GPIO_Pin_6 is MISO signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*GPIO_Pin_7 is MOSI signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*GPIO_Pin_5 is SCK signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*GPIO_Pin_4 is slected as hardware controlled CS signal.*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Hard;
/*Slave should not care this parameter.*/
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
u8 Spi_Read_Write_Byte(u8 TxData)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, TxData);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1);
}
void My_Spi_Read_Write(u8* TxBuffer,u8* RxBuffer,u16 NumOfBytes)
{
u16 i;
for(i=0;i<NumOfBytes;i++)
{
RxBuffer[i]=Spi_Read_Write_Byte(TxBuffer[i]);
}
}
/*main.c(SLAVE)*/
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include "spi.h"
u8 sendData[10]={51,61,71,81,91,101,111,121,131,141};
u8 receivedData[10]={0,0,0,0,0,0,0,0,0,0};
int main(void)
{
u8 key=0;
u8 i=0;
delay_init();
uart_init(USART1,115200);
LED_Init();
KEY_Init();
My_Spi_Init();
printf("SPI full-duplex communication test start. This is slave.\r\n");
while(1)
{
key=KEY_Scan(0);
if(key==KEY0_PRES)
{
printf("KEY0 is pressed.\r\n");
My_Spi_Read_Write(sendData,receivedData,10);
printf("The received data is:.\r\n");
for(i=0;i<10;i++)
{
printf("%d.\r\n",receivedData[i]);
}
key=0;
}
else
{
/*LED twinkle*/
GPIO_WriteBit(GPIOB, GPIO_Pin_5, !GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5));
delay_ms(200);
}
}
}
两块板子的硬件连线以及测试结果见图14和图15。
因为
S
P
I
SPI
SPI协议和
I
2
S
(
I
n
t
e
r
−
I
C
S
o
u
n
d
)
I^2S (Inter-IC\ Sound)
I2S(Inter−IC Sound)协议有点类似,
S
T
M
2
STM2
STM2芯片也是将这两个协议整合到一个模块里面的,因此接下来也大概了解一些
I
2
S
I^2S
I2S协议的基础内容。
I
2
S
I^2S
I2S协议是一种串行总线接口,主要用于传输音频数据,实现数据音频设备之间的通信。该协议由飞利浦公司在1986年提出,现在归恩智浦公司所有。这是因为恩智浦公司以前是飞利浦公司的半导体事业部,只不过后来独立了出来。所以就归恩智浦公司所有。
I
2
S
I^2S
I2S协议的通信过程主要需要三根线:
- 时钟线,“continuous serial clock (SCK)”
- 字选择线,也就是告诉此时传输的是左通道还是右通道的数据,“word select (WS)”.0 = Left channel, 1 = Right channel
- 分时复用数据线, “serial data (SD)”.
I 2 S I^2S I2S协议中发送方和接收方使用同样的时钟信号线,如果发送方作为主,则发送方必须提供时钟信号,字选择信号以及实际的数据。如图16所示。如果接收方作为主,则接收方必须提供时钟信号,字选择信号,发送方提供实际的数据。如图17所示。在复杂的系统之中可能会有多个发送方和接收方,这样就很难定义主,一般在这种系统中会有一个专门用来控制音频数据流的主,它负责产生时钟信号和字选择信号来,在它的控制之下,发送方向接收方发送数据,此时发送方和接收方都成为了从。如图18所示。 I 2 S I^2S I2S协议的一个简单的时序图如图19所示。
数据线在传输数据的时候
M
S
B
MSB
MSB优先,这是因为发送方和接收方可能有不同的字长。发送方没必要知道接收方一次可以处理多少比特的数据,接收方也没必要知道发送方一次要发送多少比特的数据。如果发送方被要求一次发送的比特数超过了它一次可以发送的比特数,那么超过它可以处理的比特数的
L
S
B
LSB
LSB将会被丢弃。如果发送方被要求一次发送的比特数少于它一次可以发送的比特数,那么剩余的
L
S
B
LSB
LSB空间位将会被填充0。如果接收方收到的比特数超过了它一次可以处理的比特数,那么超过它可以处理的比特数的
L
S
B
LSB
LSB将会被丢弃。如果接收方收到的比特数少于它一次可以处理的比特数,那么剩余的
L
S
B
LSB
LSB空间位将会被填充0。比较详细的数据通信过程以及格式可以看一下
S
T
M
32
STM32
STM32的数据手册的
S
u
p
p
o
r
t
e
d
a
u
d
i
o
p
r
o
t
o
c
o
l
s
Supported\ audio\ protocols
Supported audio protocols小节。
M
S
B
MSB
MSB可以在
W
S
WS
WS信号改变后的第二个时钟周期发送,如图19所示。也可以在
W
S
WS
WS信号改变后马上发送,也就是第一个时钟周期发送。更详细的
I
2
S
I^2S
I2S协议说明请参考官方说明文档。