11 SPI通信
- I2C通信的优缺点:
- 优点:在硬件上使用最少的通信线,实现软件上最多的功能;
- 缺点:由于I2C开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力较弱,这会导致通信线由低电平变成高电平的时候,上升沿耗时比较长,这会限制I2C的最大通信速度。所以I2C的标准模式只有100KHz的时钟频率,I2C的快速模式也只有400KHz。虽然I2C协议后面通过改进电路的方式设计出了高速模式,可以达到3.4MHz,但是目前该模式的普及程度不是很高,所以一般认为I2C的时钟速度最多就是400KHz,这个速度相对于SPI是慢了很多的。
11.1 SPI通信简介
-
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线;
-
四根通信线:
- SCK(Serial Clock):串行时钟线;
- 也可以称作SCLK、CLK、CK;
- MOSI(Master Output Slave Input):主机输出,从机输入;
- 也可以称作DO(Data Output);
- MISO(Master Input Slave Output):主机输入,从机输出;
- 也可以称作DI(Data Input);
- SS(Slave Select):从机选择;
- 也可以称作NS(Not Slave Select)、CS(Chip Select);
- SCK(Serial Clock):串行时钟线;
-
同步,全双工;
- SCK就是用来提供时钟信号的,相当于I2C通信协议中的SCL;
- MOSI和MISO就是分别用于发送和接收的两条全双工线路;
-
支持总线挂载多设备,只一主多从;
- 通过SS从机选择线来选择通信从机;
-
相对于I2C的优缺点:
- SPI传输速度快,SPI通信协议并没有严格规定最大传输速度,这个最大传输速度取决于芯片厂商的设计需求;
- 其设计简单粗暴,实现的功能比I2C少;
- SPI硬件开销大,通信线个数较多,且通信过程中经常会有资源浪费的情况;
11.2 SPI硬件电路
-
所有SPI设备的SCK、MOSI、MISO分别连在一起;
-
主机另外引出多条SS控制线,分别接到各从机的SS引脚;
- 当从机的SS引脚为高电平时,也就是从机未被选中时,其MISO引脚必须切换为高阻态,相当于引脚断开,不输出任何电平,这样就可以防止一条MISO线上有多个从机的输出而导致电平冲突的问题;
- 在SS为低电平时,MISO才允许变成推挽输出;
-
输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入;
11.3 移位示意图
- 两个移位寄存器都有一个时钟输入端,因为SPI一般都是高位先行,所以每来一个时钟,移位寄存器都会向左进行移位;
- 移位寄存器的时钟源,是有主机的波特率发生器提供的,其产生的时钟驱动主机的移位寄存器进行移位,同时这个时钟也通过SCK引脚进行输出,接到从机的移位寄存器中;
- 主机移位寄存器移出去的数据,通过MOSI引脚,输入到从机移位寄存器的右边;从机移位寄存器移出去的数据,通过MISO引脚,输入到主机移位寄存器的右边。这其实是一个字节交换的过程,实现了主机发送数据的同时接收数据的目的;
- 如果只想发送,不想接收,那么不理会从机发送过来的数据就好了;
- 如果只想接收,不想发送,那么给从机随便发送一个字节的数据,只需要把从机的数据置换过来就好了;
11.4 SPI时序基本单元
-
起始条件:SS从高电平切换到低电平,表示选中某个从机;
-
终止条件:SS从低电平切换到高电平,表示结束某个从机的选中状态;
-
交换一个字节(模式0);
- 与模式1的区别:将MOSI和MISO中数据变换的时机提前了;
- CPOL=0:空闲状态时,SCK为低电平;
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据;
- CPHA指的是时钟相位,决定是第一个时钟采样移入,还是第二个时钟采样移入,并不是规定是上升沿采样还是下降沿采样。在CPOL确定的情况下,CPHA会改变采样时刻的上升沿和下降沿。模式0和模式3,都是SCK上升沿采样;模式1和模式2,都是SCK下降沿采样;
-
交换一个字节(模式1);
- CPOL=0:空闲状态时,SCK为低电平;
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据;
-
交换一个字节(模式2);
- 与模式0的区别:二者的CPOL不一致,在波形上的体现就是模式2的SCK波形是模式0的取反;
- CPOL=1:空闲状态时,SCK为高电平;
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据;
-
交换一个字节(模式3);
- 与模式1的区别:二者的CPOL不一致,在波形上的体现就是模式3的SCK波形是模式1的取反;
- CPOL=1:空闲状态时,SCK为高电平;
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据;
11.5 SPI时序
-
SPI对字节流功能的规定与I2C不同;
- I2C规定:有效数据流的第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型;
- SPI规定:采用的是指令码+读写数据的模型,SPI起始后,第一个交换发送给从机的数据是指令码。在从机中,对应的有一个指令集;
-
发送指令:向SS指定的设备,发送指令(0x06,在W25Q64中,代表写使能);
- 使用的是SPI模式0;
- 在空闲状态时,SS为高电平,SCK为低电平,MOSI和MISO的默认电平没有严格规定;
- SS产生下降沿,时序开始,此时MOSI和MISO开始变换数据;
- 随后开始交换一个字节,因为写使能是单独的指令,不需要跟随数据,SPI只需要交换一个字节即可;
- 最后在SCK下降沿结束后,SS置回高电平,结束通信;
- 总结:主机用0x06换来了从机的0xFF,该0xFF没有意义,不用理会。随后从机会根据接收到的0x06,发现是写使能,那么从机就会控制硬件,进行写使能;
-
指定地址写:向SS指定的设备,发送写指令(0x02), 随后在指定地址(Address[23:0])下,写入指定数据(Data);
-
指定地址读:向SS指定的设备,发送读指令(0x03), 随后在指定地址(Address[23:0])下,读取从机数据(Data);
11.6 W25Q64简介
-
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景;
-
存储介质:Nor Flash(闪存);
-
时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI);
-
存储容量(24位地址):
- W25Q40:4Mbit / 512KByte;
- W25Q80:8Mbit / 1MByte;
- W25Q16:16Mbit / 2MByte;
- W25Q32:32Mbit / 4MByte;
- W25Q64:64Mbit / 8MByte;
- W25Q128:128Mbit / 16MByte;
- W25Q256:256Mbit / 32MByte;
11.7 W25Q64硬件电路
引脚 | 功能 |
---|---|
VCC、GND | 电源(2.7~3.6V) |
CS(SS) | SPI片选 |
CLK(SCK) | SPI时钟 |
DI(MOSI) | SPI主机输出从机输入 |
DO(MISO) | SPI主机输入从机输出 |
WP | 写保护 |
HOLD | 数据保持 |
11.8 W25Q64框图
- 右边的大方框是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址;
- W25Q64的地址宽度是24位3个字符,左下角第一个字节是000000h,h代表16进制。之后的空间,地址依次自增,直到最后一个字节,地址是7FFFFFh;
- 以64KB为一个基本单元,将整个存储器划分为若干个块Block,8MB/1024/64=128块,编号从块0~块127;
- 左上是对每一块进行更细的划分,将一块划分为多个扇区Sector;
- 在一块里,以4KB为一个基本单元划分为一个个扇区,64KB/4KB=16份扇区,编号从扇区0~扇区15;
- 在写入数据时,还有更细的划分,即页Page,一页是256个字节,4KB*1024/256=16页;
- 左下角是SPI控制逻辑,其左边是SPI的通信引脚,与主控芯片相连;
- 主控芯片通过SPI协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路;
- Status Register:状态寄存器,芯片是否处于忙状态、是否写使能、是否写保护,都在状态寄存器中体现;
- Write Control Logic:写控制逻辑,和外部的WP引脚相连,显示是与WP引脚实现硬件写保护的;
- High Vottage Generators:高电压生成器,是配合Flash进行编程的;
- Page Address Latch/Counter:页地址锁存/计数器;
- Byte Address Latch/Counter:字节地址锁存/计数器,与上面那个一起用来指定地址;
- 通过SPI,总共发过来3个字节的地址;
- 因为一页是256字节,所以一页内的字节地址取决于最低一个字节,而高位的两个字节就是对应的页地址;
- 所以发送过来的3个字节的地址,前两个字节就会进入页地址锁存/计数器,最后一个字节会进入字节地址锁存/计数器;
- 页地址通过写保护和行解码,来选择要操作哪一页;
- 字节地址通过列解码和256字节页缓存,来进行指定字节的读写操作;
- 因为这两个地址锁存都带有一个计数器,所以地址指针在读写之后,可以自动加1,这样就可以实现从指定地址开始,连续读写多个字节的目的;
- Column Decode And 256-Byte Page Buffer:256字节的页缓冲区,其实是一个256字节的RAM存储器,数据读写就是通过这个RAM缓冲区进行的;
- 写入数据,会先放到缓冲区里,然后在时序结束后,芯片再将缓冲区的数据复制到对应的Flash里,进行永久保存;
- 为什么不可以直接往Flash中写,而是需要经过一个缓冲区?因为SPI写入的频率非常高,而Flash的写入由于需要掉电不丢失,写入的就比较慢,所以需要一个缓冲区缓冲;
11.9 Flash操作注意事项
- 写入操作时:
- 写入操作前,必须先进行写使能;
- 每个数据位只能由1改写为0,不能由0改写为1;
- 写入数据前必须先擦除,擦除后,所有数据位变为1;
- 擦除必须按最小擦除单元(一个扇区,4KB)进行;
- 连续写入多字节时,最多写入一页(256字节)的数据,超过页尾位置的数据,会回到页首覆盖写入;
- 因为缓冲区大小就是256字节;
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作;
- 读取操作时:
- 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
11.10 软件SPI读写W24Q64
-
项目目录结构:
-
MySPI.h
:#ifndef __MYSPI_H #define __MYSPI_H void MySPI_Init(void); void MySPI_Start(void); void MySPI_Stop(void); uint8_t MySPI_SwapByte(uint8_t ByteSend); #endif
-
MySPI.c
:#include "stm32f10x.h" // Device header /*引脚配置层*/ /** * 函 数:SPI写SS引脚电平 * 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平 */ void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平 } /** * 函 数:SPI写SCK引脚电平 * 参 数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平 */ void MySPI_W_SCK(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平 } /** * 函 数:SPI写MOSI引脚电平 * 参 数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue为1时,需要置MOSI为高电平 */ void MySPI_W_MOSI(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性 } /** * 函 数:I2C读MISO引脚电平 * 参 数:无 * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1 * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1 */ uint8_t MySPI_R_MISO(void) { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回 } /** * 函 数:SPI初始化 * 参 数:无 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化 */ void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4、PA5和PA7引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 MySPI_W_SCK(0); //SCK默认低电平 } /*协议层*/ /** * 函 数:SPI起始 * 参 数:无 * 返 回 值:无 */ void MySPI_Start(void) { MySPI_W_SS(0); //拉低SS,开始时序 } /** * 函 数:SPI终止 * 参 数:无 * 返 回 值:无 */ void MySPI_Stop(void) { MySPI_W_SS(1); //拉高SS,终止时序 } /** * 函 数:SPI交换传输一个字节,使用SPI模式0 * 参 数:ByteSend 要发送的一个字节 * 返 回 值:接收的一个字节 */ uint8_t MySPI_SwapByte(uint8_t ByteSend) { uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到 for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据 { /*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/ MySPI_W_MOSI(!!(ByteSend & (0x80 >> i))); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线 MySPI_W_SCK(1); //拉高SCK,上升沿移出数据 if (MySPI_R_MISO()){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量 //当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0 MySPI_W_SCK(0); //拉低SCK,下降沿移入数据 } return ByteReceive; //返回接收到的一个字节数据 }、
-
W25Q64_Ins.h
:#ifndef __W25Q64_INS_H #define __W25Q64_INS_H #define W25Q64_WRITE_ENABLE 0x06 #define W25Q64_WRITE_DISABLE 0x04 #define W25Q64_READ_STATUS_REGISTER_1 0x05 #define W25Q64_READ_STATUS_REGISTER_2 0x35 #define W25Q64_WRITE_STATUS_REGISTER 0x01 #define W25Q64_PAGE_PROGRAM 0x02 #define W25Q64_QUAD_PAGE_PROGRAM 0x32 #define W25Q64_BLOCK_ERASE_64KB 0xD8 #define W25Q64_BLOCK_ERASE_32KB 0x52 #define W25Q64_SECTOR_ERASE_4KB 0x20 #define W25Q64_CHIP_ERASE 0xC7 #define W25Q64_ERASE_SUSPEND 0x75 #define W25Q64_ERASE_RESUME 0x7A #define W25Q64_POWER_DOWN 0xB9 #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3 #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB #define W25Q64_MANUFACTURER_DEVICE_ID 0x90 #define W25Q64_READ_UNIQUE_ID 0x4B #define W25Q64_JEDEC_ID 0x9F #define W25Q64_READ_DATA 0x03 #define W25Q64_FAST_READ 0x0B #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B #define W25Q64_FAST_READ_DUAL_IO 0xBB #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B #define W25Q64_FAST_READ_QUAD_IO 0xEB #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3 #define W25Q64_DUMMY_BYTE 0xFF #endif
-
W25Q64.h
:#ifndef __W25Q64_H #define __W25Q64_H void W25Q64_Init(void); void W25Q64_ReadID(uint8_t *MID, uint16_t *DID); void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count); void W25Q64_SectorErase(uint32_t Address); void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count); #endif
-
W25Q64.c
:#include "stm32f10x.h" // Device header #include "MySPI.h" #include "W25Q64_Ins.h" /** * 函 数:W25Q64初始化 * 参 数:无 * 返 回 值:无 */ void W25Q64_Init(void) { MySPI_Init(); //先初始化底层的SPI } /** * 函 数:MPU6050读取ID号 * 参 数:MID 工厂ID,使用输出参数的形式返回 * 参 数:DID 设备ID,使用输出参数的形式返回 * 返 回 值:无 */ void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令 *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位 *DID <<= 8; //高8位移到高位 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64写使能 * 参 数:无 * 返 回 值:无 */ void W25Q64_WriteEnable(void) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64等待忙 * 参 数:无 * 返 回 值:无 */ void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 Timeout = 100000; //给定超时计数时间 while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位 { Timeout --; //等待时,计数值自减 if (Timeout == 0) //自减到0后,等待超时 { /*超时的错误处理代码,可以添加到此处*/ break; //跳出等待,不等了 } } MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64页编程 * 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于写入数据的数组 * 参 数:Count 要写入数据的数量,范围:0~256 * 返 回 值:无 * 注意事项:写入的地址范围不能跨页 */ void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据 } MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64扇区擦除(4KB) * 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF * 返 回 值:无 */ void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64读取数据 * 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回 * 参 数:Count 要读取数据的数量,范围:0~0x800000 * 返 回 值:无 */ void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据 } MySPI_Stop(); //SPI终止 }
-
main.c
:#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "W25Q64.h" uint8_t MID; //定义用于存放MID号的变量 uint16_t DID; //定义用于存放DID号的变量 uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组 uint8_t ArrayRead[4]; //定义要读取数据的测试数组 int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 W25Q64_Init(); //W25Q64初始化 /*显示静态字符串*/ OLED_ShowString(1, 1, "MID: DID:"); OLED_ShowString(2, 1, "W:"); OLED_ShowString(3, 1, "R:"); /*显示ID号*/ W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号 OLED_ShowHexNum(1, 5, MID, 2); //显示MID OLED_ShowHexNum(1, 12, DID, 4); //显示DID /*W25Q64功能函数测试*/ W25Q64_SectorErase(0x000000); //扇区擦除 W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中 W25Q64_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中 /*显示数据*/ OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); //显示写入数据的测试数组 OLED_ShowHexNum(2, 6, ArrayWrite[1], 2); OLED_ShowHexNum(2, 9, ArrayWrite[2], 2); OLED_ShowHexNum(2, 12, ArrayWrite[3], 2); OLED_ShowHexNum(3, 3, ArrayRead[0], 2); //显示读取数据的测试数组 OLED_ShowHexNum(3, 6, ArrayRead[1], 2); OLED_ShowHexNum(3, 9, ArrayRead[2], 2); OLED_ShowHexNum(3, 12, ArrayRead[3], 2); while (1) { } }
11.11 SPI外设简介
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担;
- 可配置8位/16位数据帧、高位先行/低位先行;
- 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256);
- PCLK:外设时钟,APB2的PCLK是72MHz,APB1的PCLK是36MHz;
- SPI1挂载在APB2,SPB2挂载在APB1;
- 支持多主机模型、主或从操作;
- 可精简为半双工/单工通信;
- 支持DMA;
- 兼容I2S协议;
- I2S是一种音频传输协议;
- STM32F103C8T6 硬件SPI资源:SPI1、SPI2。
11.12 SPI框图
- LSBFIRST控制位:可以控制是低位先行还是高位先行;
- MOSI和MISO右边的交叉电路:用来进行主从引脚模式变化的(此处的图可能有错);
- 波特率发生器:用来产生SCK时钟;
11.13 SPI基本结构
11.14 主模式全双工连续传输
- 因为SPOL=1,CPHA=1,所以使用的是SPI模式3;
11.15 非连续传输
- 因为SPOL=1,CPHA=1,所以使用的是SPI模式3;
11.16 软件/硬件波形对比
11.17 硬件SPI读写W24Q64
-
项目目录结构:
-
MySPI.h
:#ifndef __MYSPI_H #define __MYSPI_H void MySPI_Init(void); void MySPI_Start(void); void MySPI_Stop(void); uint8_t MySPI_SwapByte(uint8_t ByteSend); #endif
-
MySPI.c
:- 删除
void MySPI_W_SCK(uint8_t BitValue)
、void MySPI_W_MOSI(uint8_t BitValue)
、uint8_t MySPI_R_MISO(void)
三个函数; - 将
MySPI_Init
函数中的内容替换为SPI外设的初始化; - 将
MySPI_SwapByte
函数中的内容替换为硬件SPI的代码;
#include "stm32f10x.h" // Device header /** * 函 数:SPI写SS引脚电平,SS仍由软件模拟 * 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平 */ void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平 } /** * 函 数:SPI初始化 * 参 数:无 * 返 回 值:无 */ void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA5和PA7引脚初始化为复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入 /*SPI初始化*/ SPI_InitTypeDef SPI_InitStructure; //定义结构体变量 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7 SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1 /*SPI使能*/ SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 } /** * 函 数:SPI起始 * 参 数:无 * 返 回 值:无 */ void MySPI_Start(void) { MySPI_W_SS(0); //拉低SS,开始时序 } /** * 函 数:SPI终止 * 参 数:无 * 返 回 值:无 */ void MySPI_Stop(void) { MySPI_W_SS(1); //拉高SS,终止时序 } /** * 函 数:SPI交换传输一个字节,使用SPI模式0 * 参 数:ByteSend 要发送的一个字节 * 返 回 值:接收的一个字节 */ uint8_t MySPI_SwapByte(uint8_t ByteSend) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待发送数据寄存器空 SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器,开始产生时序 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待接收数据寄存器非空 return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回 }
- 删除
-
main.c
:#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "W25Q64.h" uint8_t MID; //定义用于存放MID号的变量 uint16_t DID; //定义用于存放DID号的变量 uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组 uint8_t ArrayRead[4]; //定义要读取数据的测试数组 int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 W25Q64_Init(); //W25Q64初始化 /*显示静态字符串*/ OLED_ShowString(1, 1, "MID: DID:"); OLED_ShowString(2, 1, "W:"); OLED_ShowString(3, 1, "R:"); /*显示ID号*/ W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号 OLED_ShowHexNum(1, 5, MID, 2); //显示MID OLED_ShowHexNum(1, 12, DID, 4); //显示DID /*W25Q64功能函数测试*/ W25Q64_SectorErase(0x000000); //扇区擦除 W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中 W25Q64_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中 /*显示数据*/ OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); //显示写入数据的测试数组 OLED_ShowHexNum(2, 6, ArrayWrite[1], 2); OLED_ShowHexNum(2, 9, ArrayWrite[2], 2); OLED_ShowHexNum(2, 12, ArrayWrite[3], 2); OLED_ShowHexNum(3, 3, ArrayRead[0], 2); //显示读取数据的测试数组 OLED_ShowHexNum(3, 6, ArrayRead[1], 2); OLED_ShowHexNum(3, 9, ArrayRead[2], 2); OLED_ShowHexNum(3, 12, ArrayRead[3], 2); while (1) { } }