【STM32F1标准库】代码——SPI通信
目录
一、W25Q64介绍与使用
1.W25Q64内部结构
2.W25Q64指令集
3.W25Q64使用流程
4.W25Q64使用注意事项
二、使用软件模拟SPI时序
1.SPI时序层
0)引脚配置
1)开始
2)结束
3)交换一个字节(以符合从机的高位先行为例)
2.W25Q64驱动层(均为设备私有时序)
三、使用硬件SPI接口
1.配置流程
2.代码示例
一、W25Q64介绍与使用
W25Q64是一款由华邦公司生产的64M-bit串行Flash存储器芯片。
1.W25Q64内部结构
-
页:256字节。这是编程(写入) 的最小单位。一次最多可以连续写入一页的数据。
-
扇区:4KB。这是擦除的最小单位。在写入新数据前,必须先擦除对应的扇区。
-
块:64KB。一个大容量的擦除单位,可以一次性擦除整个块。
-
整个芯片:8MB。可以执行整片擦除命令。
一页可以存储256字节数据,刚好是2^8,所以用一字节刚好可以描述一页内每一个字节数据的地址。
2.W25Q64指令集
指令名 | 指令码 | 描述 |
---|---|---|
写使能 | 0x06 | 在执行任何写/擦除操作前,必须发送此命令! |
写禁止 | 0x04 | 禁用写操作。 |
读数据 | 0x03 | 从指定地址开始读取数据。 |
页编程 | 0x02 | 向指定地址开始的页写入数据(最多256字节)。 |
扇区擦除 | 0x20 | 擦除一个4KB的扇区。写入前必须先擦除! |
芯片擦除 | 0xC7 | 擦除整个芯片,时间较长。 |
读状态寄存器 | 0x05 | 读取状态寄存器,主要用于检查BUSY位。 |
读取设备ID | 0x9F | 读JEDEC ID(返回制造商ID和设备ID) |
任何写/擦除操作后,都必须轮询检查BUSY位,直到它变为0,才能进行下一步操作。
3.W25Q64使用流程
- 读取器件ID(验证通信)
- 擦除扇区(在写入新数据前,必须先擦除目标扇区)
- 写入/读取数据
4.W25Q64使用注意事项
-
先擦后写:Flash存储器只支持将1改写为0,不支持将0改写为1,因此在写入数据之前,必须通过擦除将整个扇区/块恢复为
1
。 -
写前使能:任何改变数据的操作(编程、擦除)前,都必须发送
0x06
写使能命令。 -
检查忙状态:在写/擦除操作前(或者事后判断),必须等待BUSY位清零,才能进行下一步操作。
-
页边界:页编程不能跨页。如果你要写入的数据跨越了页边界会回到页首覆盖页首的数据,所以对于超过一页的数据进行存储,需要拆分成两次页编程操作。
-
读取操作:直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
-
驱动优化:对于大量数据的读写,可以考虑使用DMA来提高效率,避免CPU被长时间占用。
二、使用软件模拟SPI时序
1.SPI时序层
0)引脚配置
/****************************** 片选线电平控制 ******************************/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}/****************************** 时钟线电平控制 ******************************/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平
}/****************************** MOSI线电平控制 ******************************/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}/****************************** MISO线电平检测 ******************************/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取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默认低电平
}
1)开始
主机将与从机相连的片选线SS线由高电平拉低到低电平,表示选中该从机进行通信,整个通信期间SS线都保持在低电平状态
void MySPI_Start(void)
{MySPI_W_SS(0); //拉低SS,开始时序
}
2)结束
void MySPI_Stop(void)
{MySPI_W_SS(1); //拉高SS,终止时序
}
3)交换一个字节(以符合从机的高位先行为例)
- 模式0(常用)
- 第一个时钟脉冲还没有到就需要把数据放到数据线上
- SCK下降沿将数据从寄存器移出到数据线上
- SCK上升沿将数据从数据线移入到寄存器中
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00作为与从机的交换数据,此处必须赋初值0x00,后面会用到for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据{MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1); //拉高SCK,上升沿移出数据if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0MySPI_W_SCK(0); //拉低SCK,下降沿移入数据}return ByteReceive; //返回接收到的一个字节数据
}
2.W25Q64驱动层(均为设备私有时序)
/****************************** 引脚初始化 ******************************/
void W25Q64_Init(void)
{MySPI_Init(); //先初始化底层的SPI
}/****************************** 读取设备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终止
}/********************************* 写使能 *********************************/
void W25Q64_WriteEnable(void)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令MySPI_Stop(); //SPI终止
}/**************************** 等待BUSY标志位清零 ****************************/
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终止
}/****************************** 指定扇区数据擦除 ******************************/
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(); //等待忙
}/****************************** 指定页写入数据 ******************************/
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(); //等待忙
}/****************************** 指定页读取数据 ******************************/
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终止
}
三、使用硬件SPI接口
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
- 可配置8位/16位数据帧、高位先行/低位先行
- 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
- 支持多主机模型、主或从操作
- 可精简为半双工/单工通信
- 支持DMA
- 兼容I2S协议
- STM32F103C8T6 硬件SPI资源:SPI1、SPI2
1.配置流程
- 开启时钟
- 将引脚配置为复用推挽输出
- 初始化SPI
- 使能SPI
- 调用库函数,使用数据寄存器中的数据与从设备进行数据交换
2.代码示例
仅在SPI时序层做出改动,使用硬件SPI代替软件SPI
W5Q64层代码与软件SPI代码一致
/****************************** 片选线电平控制 ******************************/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平
}/****************************** 发起开始时序 ******************************/
void MySPI_Start(void)
{MySPI_W_SS(0);
}/****************************** 发起结束时序 ******************************/
void MySPI_Stop(void)
{MySPI_W_SS(1);
}/****************************** 硬件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模式0SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1/*SPI使能*/SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行/*设置默认电平*/MySPI_W_SS(1); //SS默认高电平
}/****************************** 交换一个字节 ******************************/
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); //读取接收到的数据并返回
}