wordpress 4.9 多站,wordpress创意博客,公众号文章排版,安阳县属于哪个省哪个市目录
SPI通信简介
硬件电路
移位示意图
SPI基本时序单元
SPI时序
W25Q64简介
硬件电路
W25Q64框图
Flash操作注意事项
SPI外设简介
SPI框图
SPI基本结构
主模式全双工连续传输
非连续传输
软件/硬件波形对比
SPI应用
软件SPI读写W25Q64
硬件SPI读写W25Q64 SP…目录
SPI通信简介
硬件电路
移位示意图
SPI基本时序单元
SPI时序
W25Q64简介
硬件电路
W25Q64框图
Flash操作注意事项
SPI外设简介
SPI框图
SPI基本结构
主模式全双工连续传输
非连续传输
软件/硬件波形对比
SPI应用
软件SPI读写W25Q64
硬件SPI读写W25Q64 SPI通信简介
SPISerial Peripheral Interface是由Motorola公司开发的一种通用数据总线四根通信线SCKSerial Clock串行时钟线、MOSIMaster Output Slave Input主机输出从机输入、MISOMaster Input Slave Output主机输入从机输出、SSSlave Select从机选择同步全双工支持总线挂载多设备一主多从
SPI相对于I2C来说SPI传输更快SPI协议没有严格规定最大传输速度最大传输速度取决于芯片设计厂商设计需求比如W25Q64芯片手册里写的SPI时钟频率最大可达80MHz比STM32主频还高其次SPI的设计比较简单粗暴功能没有I2C那么多学习起来比I2C简单最后SPI的硬件开销比较大通信线的个数比较多并且通信过程中经常有资源浪费现象。
SCK有些地方也叫SCLK、CLK、CK。MOSI和MISO有的地方可能直接叫DO和DI。SS有的地方也叫NSS、CS。
SCK引脚提供时钟信号数据位的输出和输入都是在SCK的上升沿或者下降沿进行。
SS从机选择线有几个从机就开几条SS需要哪个从机就控制接到对应从机的SS给个低电平说明要找这个从机了。
硬件电路
所有SPI设备的SCK、MOSI、MISO分别连在一起主机另外引出多条SS控制线分别接到各从机的SS引脚输出引脚配置为推挽输出输入引脚配置为浮空或上拉输入 单端信号需要共地图上GND没画出来但是是必须要接的。从机没有独立供电主机还要引出电源VCC给从机供电。
SCK时钟线完全由主机掌控对主机来说时钟线为输出对于所有从机来说时钟线都为输入这样主机的同步时钟就能送到各个从机了。
MOSI主机输出从机输入数据传输方向是主机通过MOSI输出所有从机通过MOSI 输入MISO主机输入从机输出数据传输方向是三个从机通过MISO输出主机通过MISO输入。
主机的SS线都是输出从机的SS线都是输入SS线低电平有效主机想指定谁就把对应SS输出置低电平就行了。同一时间主机只能置一个SS为低电平只能选中一个从机否则会数据冲突。
推挽输出高低电平都有较强驱动能力这将使得SPI引脚信号的下降沿和上升沿都非常迅速不像I2C那样下降沿非常迅速上升沿比较缓慢得益于推挽输出的驱动能力SPI信号变化的快那自然它就能达到更高的传输速度。
I2C并不是不想使用更快的推挽输出而是I2C要实现半双工经常要切换输入输出而且I2C又要实现多主机的时钟同步和总线仲裁这些功能都不允许I2C使用推挽输出要不然一不小心就电源短路了所以I2C选择了更多的功能自然要放弃更强的性能了。
对于SPI来说首先不支持多主机然后又是全双工SPI的输出引脚始终是输出输入引脚始终是输入基本不会冲突所以可以大胆使用推挽输出不过SPI其实还是有一个冲突点就是图上的MISO引脚主机一个是输入三个从机全都是输出如果三个从机都始终是推挽输出那势必会导致冲突所以SPI协议有一条规定当从机的SS引脚为高电平也就是从机未被选中时它的MISO引脚必须切换为高阻态高阻态就相当于引脚断开不输出任何电平这样就可以防止一条线有多个输出而导致电平冲突的问题了在SS为低电平时MISO才允许变为推挽输出这个切换过程一般在从机里我们一般都是写主机的程序所以我们主机的程序不需要关注这个问题。
移位示意图 接下来演示电路如何工作
首先我们规定波特率发生器时钟的上升沿所有移位寄存器向左移动一位移出去的数据位放到引脚上波特率发生器时钟的下降沿引脚上的位采样输入到移位寄存器的最低位。
接下来假设主机有个数据1010 1010要发送到从机同时从机有个数据0101 0101要发送到主机那我们就可以驱动时钟先产生一个上升沿这时所有的为向左移动一次从最高位移出去的数据放到通信线上实际就是放到了输出数据寄存器 上图就是第一个时钟上升沿执行的结果就是把主机和从机中移位寄存器的最高位分别放到MOSI和MISO的通信线上。
之后时钟继续运行上升沿之后下一个边沿就是下降沿下降沿时主机和从机内都会进行数据采样输入 这就是第一个时钟结束后的现象。那时钟继续运行下一个上升沿同样的操作移位输出 随后下降沿数据采样输入 最终8个时钟之后主机原来的1010 1010跑到从机里了原来从机里的0101 0101跑到主机里了这就实现了主机和从机一个字节的数据交换 SPI的数据收发都是基于字节交换这个基本单元来进行的当主机需要发送一个字节并且同时需要接收一个字节时就可以执行一下字节交换的时序如果只想发送不想接收仍然调用交换字节的时序发送同时接收只是这个接收到的数据我们不看它就行了 如果只想接收不想发送还是调用交换字节的时序发送同时接收只是我们会随便发送一个数据只要能把从机的数据置换过来就好了我们读取置换过来的数据就是接收了这里我们随便发过去的数据从机也不会看它一般这个随便的数据是0x00或者0xFF。
SPI基本时序单元
起始条件SS从高电平切换到低电平终止条件SS从低电平切换到高电平 接下来就是数据传输的基本单元了这个基本单元建立在我们刚才说的移位模型上并且什么时候开始移位上升沿移位还是下降沿移位SPI并没有限定死给了我们可以配置的选择这样SPI就能兼容更多芯片。
SPI有两个可配置的位分别是CPOLClock Polarity时钟极性CPHAClock Phase时钟相位每一位可以配置为1或0总共组合起来就有模式0、模式1、模式2、模式3四种模式。
先看模式1这个模式与前文提到的移位模型是对应的 主机通过MOSI移出最高位此时MOSI的电平就表示主机想要发送的数据的B7从机通过MISO移出最高位此时MISO表示从机想要发送的数据的B7然后时钟运行产生下降沿此时主机和从机同时移入数据也就是进行数据采样这里主机移出的B7进入从机移位寄存器最低位从机移出的B7进入主机移位寄存器最低位这样一个时钟脉冲产生完毕一个数据位传输完毕。接下来就是同样的过程最后一个下降沿B0传输完成自此主机和从机完成了一个字节的数据交换如果主机只想交换一个字节那这时就可以置SS为高电平结束通信了在SS上升沿MOSI还可以在变化一次将MOSI置到一个默认高电平或者低电平当然也可以不管它因为SPI没有硬性规定MOSI的默认电平然后MISO从机必须得置回高阻态此时如果主机的MISO为上拉输入那MISO引脚电平就是默认的高电平如果主机MISO为浮空输入那MISO引脚电平不确定如果主机还想继续交换字节在此时主机就不必把SS置回高电平直接重复一下交换字节的时序。
之后我们看一下模式0
SS下降沿触发了输出SCK上升沿就可以采样输入数据了这样B7就传输完毕之后SCK下降沿移出B6SCK上升沿移入B6最终在第8个上升沿B0位移入完成整个字节交换完成之后SCK还有个下降沿如果主机只需要交换一个字节就结束那在整个下降沿MOSI可以置回默认电平或者不管它MISO也会变化一次这一位实际上是下一个字节的B7因为这个相位提前了所以下一个字节的B7会露个头如果不需要的话SS上升沿之后从机MISO置回高阻态。如果主机想交换多个字节那就继续调用时序在最后一个下降沿主机放下一个字节的B7从机也放下一个字节的B7SCK上升沿正好接着采样第二个字节的B7。
模式0和模式1的区别就在于模式0把这个数据变化的时机提前了在实际应用中模式0是最常用的。
模式2和模式3 模式0和模式2的区别就是模式0的CPOL0模式2的CPOL1两者的波形就是SCK的极性取反一下剩下的流程完全一致模式1和模式3的区别也是模式1的CPOL0模式3的CPOL1两者的波形也是SCK的极性取反一下其他地方没有变化。
模式0和模式3都是SCK上升沿采样模式1和模式2都是SCK下降沿采样。
SPI时序
接下来看几个完整的SPI时序波形以W25Q64为例。SPI对字节流功能的规定不像I2CI2C规定一般是有效数据流第一个字节是寄存器地址之后依次是读写的数据使用的是读写寄存器的模型。而SPI通常采用指令码加读写数据的模型这个过程就是SPI起始后第一个交换发送给从机的数据一般叫指令码在从机中对应会有一个指令集当我们需要发送什么指令时就可以在起始后第一个字节发送指令集里面的数据这样就能指导从机完成相应的功能了不同的指令可以有不同的数据个数。 整个时序的功能就是发送指令指令码是0x06从机一比对事先定义好的指令集发现0x06是写使能的指令那从机就会控制硬件进行写使能。 地址24位分3个字节传输时序首先SS下降沿开始时序第一个字节用0x02换来了0xFF其中发送的0x02是一条指令代表这是一个写数据的时序接收的0xFF不需要看。后面跟着写的地址和数据第二个字节用0x12换来了0xFFW25Q64规定写指令之后的字节定义为地址高位所以这个0x12就表示地址的23~16位之后同理三个字节交换24位地址发送完毕从机收到的24位地址是0x123456之后发送写入的数据。 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
硬件电路 W25Q64框图 数据从缓冲区到Flash里需要一定时间所以在写入时序结束后芯片会进入一段忙的状态会有一条线通往状态寄存器的BUSY位将该位置1忙的时候芯片就不会响应新的读写时序了。
Flash操作注意事项
写入操作时
1写入操作前必须先进行写使能2每个数据位只能由1改写为0不能由0改写为13写入数据前必须先擦除擦除后所有数据位变为14擦除必须按最小擦除单元进行5连续写入多字节时最多写入一页的数据超过页尾位置的数据会回到页首覆盖写入6写入操作结束后芯片进入忙状态不响应新的读写操作读取操作时直接调用读取时序无需使能无需额外操作没有页的限制读取操作结束后不会进入忙状态但不能在忙状态时读取
SPI外设简介
STM32内部集成了硬件SPI收发电路可以由硬件自动执行时钟生成、数据收发等功能减轻CPU的负担可配置8位/16位数据帧、高位先行/低位先行时钟频率 fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)支持多主机模型、主或从操作可精简为半双工/单工通信支持DMA兼容I2S协议STM32F103C8T6 硬件SPI资源SPI1、SPI2
我们SPI最常用的配置是8位数据帧高位先行。I2S是数字音频信号传输的协议和I2C区别很大不要搞混了了解。
SPI框图 发送数据先写入TDR再转到移位寄存器发送发送的同时接收数据接收到的数据转到RDR再从RDR读取数据。
SPI基本结构 主模式全双工连续传输 非连续传输
主模式全双工连续传输比较复杂也不太方便封装在实际过程中如果对性能没有极致的追求我们更倾向于使用这个非连续传输的非连续传输更加简单实际用的话只需要4行代码就能完成任务。 按照这个我们的流程就是第一步等待TXE为1第二步写入发送的数据到TDR第三步等待RXNE为1第四步读取RDR接收到的数据之后交换第二个字节重复这四步。
非连续传输缺点就是没有及时把下一个数据写入TDR等着所以等待上一个字节时序完成后下一个字节还没有来那这个数据传输就会在这里等会拖慢数据传输速度这个间隙在SCK频率低的时候影响不大SCK频率非常高时影响就比较大。
软件/硬件波形对比 SPI应用 软件SPI读写W25Q64
软件模拟的SPI的CS、DO、CLK、DI线可以接到STM32的任意GPIO口。
程序框架如下 首先建立MySPI模块在MySPI_Init函数里初始化通信引脚接线图中我们使用PA4、5、6、7引脚开启GPIOA时钟前文提到输出引脚配置为推挽输出输入引脚配置为浮空输入或者上拉输入对于主机来说时钟、主机输出和片选都是输出引脚所以这3个脚配置为推挽输出剩下一个主机输入是输入引脚我们选择上拉输入。
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);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);
接下来把置引脚高低电平的函数都封装换个名字
/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}/*
* 写SCK的引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}/*
* 写MISO的引脚
*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}/*
* 读MISO的引脚
*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
} MySPI_Init函数里我们还有一个工作要做就是置一下初始化之后引脚的默认电平SS高电平不选中从机SCK我们计划使用SPI模式0所以默认低电平
/* 置默认电平 */
MySPI_W_SS(1);//SS默认高电平不选中从机
MySPI_W_SCK(0);//计划用SPI模式0所以默认低电平
接下来我们开始写SPI的三个时序基本单元
/*起始信号
* 直接把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 直接把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节模式0
* 参数 ByteSend通过交换一个字节发送出去的数据
* 返回值通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive 0x00;//用于接收for(i 0;i 8;i){MySPI_W_MOSI(ByteSend (0x80 i));//初始SCK低电平主机移出一位数据到MOSI(调用写MOSI)从机移出一位到MISO(从机的事不归我们管)MySPI_W_SCK(1);//SCK产生上升沿主机和从机同时移入数据从机自动把MOSI的数据读走(不归我们管)主机读取MISO的数据(上升沿后进行)if (MySPI_R_MISO() 1){ByteReceive | (0x80 i);}//主机把从机刚才放在MISO的数据位读进来MySPI_W_SCK(0);//SCK产生下降沿主机从机移出下一位数据}return ByteReceive;
}
接下来按照计划写下一个模块在SPI通信层基础上建立W25Q64驱动层。同理先在W25Q64_Init函数进行初始化 由于不需要初始化其他东西所以只调用SPI_Init即可。
void W25Q64_Init(void)
{MySPI_Init();}
现在我们来实现业务代码也就是拼接完整时序这个我们要参考手册主要参考指令集表格 每个指令对应一个指令码直接写数字意义不太明显可读性不高所以我们单独建立一个头文件W26Q64_Ins.s存放宏定义
#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前文提到写操作前必须进行写使能涉及写使能1的时序有扇区擦除和页编程为了方便使用我们直接在函数里自带写使能。还有一个就是写入操作后芯片进入忙状态所以我们在每次写操作结束后调用一下等待BUSY清零的函数。
/*读取8位厂商IDMID和16位设备IDDID
*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID);//参数是交换发送的0x9F读ID号的指令返回值是交换接收的但是这里没有意义返回值就不要了*MID MySPI_SwapByte(W25Q64_DUMMY_BYTE);//将从机发送的ID号置换过来随便发一个0xFF这个返回值是我们要的MID*DID MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID高8位*DID 8;*DID | MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID低8位MySPI_Stop();
}/*写使能
*/
void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}/*等待忙函数读状态寄存器1,一般读最低位BUSY判断是不是忙状态1表示芯片还在忙
*/
void W25Q64_WaitBusy(void)
{uint32_t TimeOut;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);TimeOut 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) 0x01) 0x01)//等待BUSY为0{TimeOut --;if(TimeOut 0)//超时退出{break;}};MySPI_Stop();
}/*页编程
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//交换发送3个字节的地址,例如0x12345616就是0x128就是0x1234舍弃高位就是0x34,不移动舍弃高位就是0x56MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);//写入for(i 0;i Count;i){MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*擦除扇区擦除
*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//指定地址MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*读取数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA);//指定地址MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);//接收for(i 0;i Count;i){DataArray[i] MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
完整代码
MySPI.c:
#include stm32f10x.h // Device header/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}/*
* 写SCK的引脚
*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}/*
* 写MISO的引脚
*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}/*
* 读MISO的引脚
*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);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);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);/* 置默认电平 */MySPI_W_SS(1);//SS默认高电平不选中从机MySPI_W_SCK(0);//计划用SPI模式0所以默认低电平}/*起始信号
* 直接把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 直接把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节模式0
* 参数 ByteSend通过交换一个字节发送出去的数据
* 返回值通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i,ByteReceive 0x00;//用于接收for(i 0;i 8;i){MySPI_W_MOSI(ByteSend (0x80 i));//初始SCK低电平主机移出一位数据到MOSI(调用写MOSI)从机移出一位到MISO(从机的事不归我们管)MySPI_W_SCK(1);//SCK产生上升沿主机和从机同时移入数据从机自动把MOSI的数据读走(不归我们管)主机读取MISO的数据(上升沿后进行)if (MySPI_R_MISO() 1){ByteReceive | (0x80 i);}//主机把从机刚才放在MISO的数据位读进来MySPI_W_SCK(0);//SCK产生下降沿主机从机移出下一位数据}return ByteReceive;
}
W25Q64.c:
#include stm32f10x.h // Device header
#include MySPI.h
#include W25Q64_Ins.h void W25Q64_Init(void)
{MySPI_Init();}/*读取8位厂商IDMID和16位设备IDDID
*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID);//参数是交换发送的0x9F读ID号的指令返回值是交换接收的但是这里没有意义返回值就不要了*MID MySPI_SwapByte(W25Q64_DUMMY_BYTE);//将从机发送的ID号置换过来随便发一个0xFF这个返回值是我们要的MID*DID MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID高8位*DID 8;*DID | MySPI_SwapByte(W25Q64_DUMMY_BYTE);//这个返回值是我们要的DID低8位MySPI_Stop();
}/*写使能
*/
void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}/*等待忙函数读状态寄存器1,一般读最低位BUSY判断是不是忙状态1表示芯片还在忙
*/
void W25Q64_WaitBusy(void)
{uint32_t TimeOut;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);TimeOut 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) 0x01) 0x01)//等待BUSY为0{TimeOut --;if(TimeOut 0)//超时退出{break;}};MySPI_Stop();
}/*页编程
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//交换发送3个字节的地址,例如0x12345616就是0x128就是0x1234舍弃高位就是0x34,不移动舍弃高位就是0x56MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);//写入for(i 0;i Count;i){MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*擦除扇区擦除
*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable();//写使能MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//指定地址MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();//事后等待
}/*读取数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA);//指定地址MySPI_SwapByte(Address 16);MySPI_SwapByte(Address 8);MySPI_SwapByte(Address);//接收for(i 0;i Count;i){DataArray[i] MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
main.c:
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include W25Q64.huint8_t MID;
uint16_t DID;uint8_t ArrayWrite[] {0x01,0x02,0x03,0x04};
uint8_t ArrayRead[4];uint8_t Key_Num;int main(void)
{OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, MID: DID:);OLED_ShowString(2, 1, W:);OLED_ShowString(3, 1, R:);W25Q64_ReadID(MID, DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);W25Q64_SectorErase(0x000000);//擦除W25Q64_PageProgram(0x000000, ArrayWrite, 4);//写入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){}
}硬件SPI读写W25Q64
硬件SPI引脚就不能任意选择了 NSS我们继续使用软件模拟的方式实现NSS没必要必须接在PA4其他三个引脚就必须是PA5、6、7了。我们计划用SPI1所以SCK接PA5MISO接PA6MOIS接PA7。
在上一个代码的MySPI底层修改即可先看一下SPI相关库函数
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);//恢复缺省配置
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);//初始化
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);//结构体初始化
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);//外设使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);//中断使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);//DMA使能
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);//写DR数据寄存器写TDR
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);//读DR数据寄存器返回RDR的值
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);//获取标志位
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);//清除标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);//获取中断标志位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);//清除中断标志位第一步开启SPI和GPIO时钟。
/* 第一步初始化GPIO与SPI时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
第二步第二步初始化GPIO口。
/* 第二步初始化GPIO口SCK和MOSI是硬件外设控制的输出信号配置为复用推挽输出MISO是硬件外设输入信号配置为上拉输入 */
/* SS是软件控制的输出信号配置为通用推挽输出 */
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);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);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); 第三步配置SPI外设。SPI_Init第一个参数SPI1第二个参数结构体
SPI_ModeSPI模式决定当前设备是SPI的主机还是从机SPI_Direction配置SPI裁剪引脚SPI_DataSize配置8位还是16位数据帧SPI_FirstBit配置高位先行还是低位先行SPI_BaudRatePrescaler波特率预分频器配置SCK时钟频率SPI_CPOL时钟极性SPI_CPHA时钟相位第几个边沿采样(移入)SPI_NSSNSS引脚SPI_CRCPolynomialCRC校验多项式
/* 第三步配置SPI外设 */
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode SPI_Mode_Master;//SPI模式决定当前设备是SPI的主机还是从机我们选主机
SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚我们用标准模式双线全双工
SPI_InitStructure.SPI_DataSize SPI_DataSize_8b;//配置8位还是16位数据帧我们选8位
SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB;//配置高位先行还是低位先行我们选高位先行
SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_128;//波特率预分频器配置SCK时钟频率
SPI_InitStructure.SPI_CPOL SPI_CPOL_Low;//时钟极性我们用SPI模式0也就是默认低电平
SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge;//时钟相位第几个边沿采样(移入)我们用SPI模式0所以我们选第一个开始采样
SPI_InitStructure.SPI_NSS SPI_NSS_Soft;//NSS引脚我们计划使用GPIO模拟外设的NSS我们不用选软件NSS就行了
SPI_InitStructure.SPI_CRCPolynomial 7;//CRC校验多项式我们不用填默认值7
SPI_Init(SPI1, SPI_InitStructure); 第四步开关控制SPI使能。
/* 第四步开关控制SPI使能 */
SPI_Cmd(SPI1, ENABLE);
最后默认SS输出高电平不选中从机。
MySPI_W_SS(0);//默认SS输出高电平不选中从机 这样SPI初始化函数就写好了SPI外设就绪我们就可以完成交换字节的函数了。调用交换字节函数硬件SPI外设就要自动控制SCK、MOSI、MISO引脚生成时序。根据前文提到的四步。
/*起始信号
* 把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节模式0
* 参数 ByteSend通过交换一个字节发送出去的数据
* 返回值通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) ! SET);//等待TXE为1SPI_I2S_SendData(SPI1, ByteSend);//软件写入数据到SPI_DR,ByteSend写入到TDR//之后ByteSend自动转入移位寄存器一旦移位寄存器有数据时序波形就会自动产生while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ! SET);//等待RXNE为1return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据就是置换接收的一个字节
} 完整代码
MySPI.c:
#include stm32f10x.h // Device header/*
* 写SS的引脚
*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}void MySPI_Init(void)
{/* 第一步初始化GPIO与SPI时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);/* 第二步初始化GPIO口SCK和MOSI是硬件外设控制的输出信号配置为复用推挽输出MISO是硬件外设输入信号配置为上拉输入 *//* SS是软件控制的输出信号配置为通用推挽输出 */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);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);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);/* 第三步配置SPI外设 */SPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_Mode SPI_Mode_Master;//SPI模式决定当前设备是SPI的主机还是从机我们选主机SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex;//配置SPI裁剪引脚我们用标准模式双线全双工SPI_InitStructure.SPI_DataSize SPI_DataSize_8b;//配置8位还是16位数据帧我们选8位SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB;//配置高位先行还是低位先行我们选高位先行SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_128;//波特率预分频器配置SCK时钟频率SPI_InitStructure.SPI_CPOL SPI_CPOL_Low;//时钟极性我们用SPI模式0也就是默认低电平SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge;//时钟相位第几个边沿采样(移入)我们用SPI模式0所以我们选第一个开始采样SPI_InitStructure.SPI_NSS SPI_NSS_Soft;//NSS引脚我们计划使用GPIO模拟外设的NSS我们不用选软件NSS就行了SPI_InitStructure.SPI_CRCPolynomial 7;//CRC校验多项式我们不用填默认值7SPI_Init(SPI1, SPI_InitStructure);/* 第四步开关控制SPI使能 */SPI_Cmd(SPI1, ENABLE);MySPI_W_SS(0);//默认SS输出高电平不选中从机}/*起始信号
* 把SS置低电平
*/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/*终止信号
* 把SS置高电平
*/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/*交换一个字节模式0
* 参数 ByteSend通过交换一个字节发送出去的数据
* 返回值通过交换一个字节接收到的数据
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) ! SET);//等待TXE为1SPI_I2S_SendData(SPI1, ByteSend);//软件写入数据到SPI_DR,ByteSend写入到TDR//之后ByteSend自动转入移位寄存器一旦移位寄存器有数据时序波形就会自动产生while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ! SET);//等待RXNE为1return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据就是置换接收的一个字节
}