SPI
SPI介绍
SPI(Serial Peripheral Interface,串行外围设备接口)是一种高速
、全双工
、同步
的串行通信总线,常用于微控制器与各种外围设备(如传感器、存储器、显示器等)之间的数据传输。SPI总线由摩托罗拉公司在20世纪80年代初提出,因其高效的通信方式被广泛应用于嵌入式系统中。
SPI通信特点
全双工通信
:SPI是一种全双工通信协议,意味着数据可以同时在两个方向上传输。主从架构
:SPI通信通常涉及一个主设备
(Master)和一个或多个从设备(Slave)
。主设备控制时钟信号和通信时序,从设备按照主设备的指令响应。同步通信
:SPI使用一个共享的 时钟信号(SCLK) 来同步数据传输。主设备生成时钟信号
,从设备根据该时钟进行数据采集和发送。
高速度
:由于使用硬件时钟同步,SPI可以实现非常高的数据传输速率。
SPI通信速度
SPI通信速度
(也称为SPI时钟频率)取决于多个因素,包括主设备和从设备的硬件能力、通信距离、电路设计等。一般来说,SPI通信速度可以从几kHz到几十MHz不等
。以下是一些常见的情况和典型值:
-
低速应用:一些简单的
传感器和外围设备
可能只需要几kHz到几百kHz
的SPI时钟频率。例如,很多低速SPI设备
在100kHz到1MHz
的范围内工作。 -
中速应用:一些存储器(如EEPROM、Flash)和显示器可能需要几MHz的SPI时钟频率。例如,常见的SPI
Flash存储器
通常支持到10MHz
或更高的频率。 -
高速应用:一些高性能设备(如高速
ADC/DAC、FPGA、快速存储器
)可以支持几十MHz
的SPI时钟频率。例如,高速Flash存储器和FPGA之间的通信可以达到50MHz
甚至更高。
SPI通信接口
SPI通信通常包括四条主要信号线
SCLK(Serial Clock)
:时钟信号,由主设备生成,控制数据传输速率。MOSI(Master Out Slave In)
:主设备发送数据线,主设备的数据通过这条线发送到从设备。
-MISO(Master In Slave Out)
:从设备发送数据线,从设备的数据通过这条线发送到主设备。SS/CS(Slave Select/Chip Select)
:从设备选择信号,低电平有效。主设备通过这条线选择具体的从设备进行通信。
SPI通信模式
SPI有四种工作模式
,通过时钟极性(CPOL)和时钟相位(CPHA)
的组合来定义:
- 模式0
:CPOL = 0,CPHA = 0(空闲时钟为低电平,数据在时钟上升沿采样)
模式1
:CPOL = 0,CPHA = 1(空闲时钟为低电平,数据在时钟下降沿采样)模式2
:CPOL = 1,CPHA = 0(空闲时钟为高电平,数据在时钟下降沿采样)模式3
:CPOL = 1,CPHA = 1(空闲时钟为高电平,数据在时钟上升沿采样)
SPI通信协议
主设备选择从设备
:主设备通过拉低对应从设备的CS信号,选择目标从设备进行通信。数据传输
:主设备生成时钟信号并在MOSI线上发送数据,同时从设备在MISO线上发送数据。数据传输按照字节为单位进行。数据采集
:在时钟信号的上升沿或下降沿,主从设备分别采集数据位。传输结束
:主设备释放CS信号(拉高),结束一次通信。
SPI优缺点
优点:
高速传输
:由于硬件时钟同步,SPI能实现比I2C更高的传输速率。简单协议
:协议简单,没有复杂的仲裁和确认机制。全双工通信
:能够实现双向数据同时传输,提高通信效率。
缺点:
占用引脚多
:每个从设备都需要一个独立的CS引脚,主设备需要更多的GPIO引脚来选择多个从设备。没有确认机制
:没有像I2C那样的ACK/NACK确认机制,数据传输可靠性需要其他手段保证。
字节交换通信
SPI(Serial Peripheral Interface)是一种基于字节交换的同步串行通信协议。在SPI通信中,数据传输是全双工的,即主设备和从设备可以同时发送和接收数据。这种数据交换方式称为“字节交换”或“位交换”。
-
选择性读取和写入
在SPI通信中,虽然数据是在每个时钟周期中双向传输的,但主设备和从设备可以选择忽略接收到的数据。例如: -
只发送数据:如果主设备只需要发送数据给从设备,而不关心接收到的数据,它可以简单地忽略接收缓冲区中的数据。
-
只接收数据:如果主设备只需要从从设备接收数据,它可以在发送过程中发送“空数据”(如0xFF)去置换从机的数据,从机此时可以选择不读(因为是无用的0xff),并只处理接收缓冲区中的数据。
SPI时序
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平
主从通信(交换一个字节)
这里以模式0为例,其他的模式类似:
如果上面的看不懂可以看下面的,两个结合去看就可以看明白了。
上面的图的解释:
-
1.
在ss片选拉低时,即第0个边沿时(提前将待移出的数据移至 各自数据线上,即移位寄存器的一个数据跑到MOSI,此时如果是1,则MOSI是高电平,反之低电平。MISO同理) -
2.
SCK上升沿时(第一个边沿)将数据移入,即MOSI的一个数据跑到MISO,MISO的一个数据跑到MOSI -
3.
SCL下降沿(第二个人边沿),将第二个数据移出,重复步骤1和2 共8次即可完成一个字节的交换。
代码编写(spi&w25q64)
mySPI.c
/*
* @Author: i want to 舞动乾坤
* @Date: 2024-07-26 17:25:14
* @LastEditors: i want to 舞动乾坤
* @LastEditTime: 2024-07-26 21:47:45
* @FilePath: \spi_software_drivet_w25q64\main\mySPI.c
* @Description:
*
* Copyright (c) 2024 by i want to 舞动乾坤, All Rights Reserved.
*/
#include <driver/gpio.h>
#include <stdint.h>
#include <esp_log.h>
#define mySPI_SS_Pin GPIO_NUM_5 //片选
#define mySPI_SCL_Pin GPIO_NUM_18 //时钟
#define mySPI_MISO_Pin GPIO_NUM_19//主机输入,从机输出
#define mySPI_MOSI_Pin GPIO_NUM_23//主机输出,从机输入
/**
* @description: 给片选信号写入高低电平
* @param {uint8_t} BitValue 写入的高低电平 (1:高电平) (0:低电平) 取值:0-1
* @return {无}
*/
void mySPI_Write_SS(uint8_t BitValue)
{
gpio_set_level(mySPI_SS_Pin,(uint8_t)BitValue);
}
/**
* @description: 给时钟信号写入高低电平
* @param {uint8_t} BitValue 写入的高低电平 (1:高电平) (0:低电平) 取值:0-1
* @return {无}
*/
void mySPI_Write_SCL(uint8_t BitValue)
{
gpio_set_level(mySPI_SCL_Pin,(uint8_t)BitValue);
}
/**
* @description:SPI写MOSI引脚电平
* @param {uint8_t} 议层传入的当前需要写入MOSI的电平,范围0~0xFF
* @tip:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
* @return {无}
*/
void MySPI_Write_MOSI(uint8_t BitValue)
{
gpio_set_level(mySPI_MOSI_Pin,(uint8_t)BitValue);
}
/**
* @description:SPI读MISO引脚电平
* @param :无
* @tip:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
* @return {协议层需要得到的当前MISO的电平,范围0~1}
*/
uint8_t mySPI_Read_MISO()
{
return (uint8_t)gpio_get_level(mySPI_MISO_Pin);
}
/**
* @description: spi初始化
* @return {无}
*/
void mySPI_Init(void)
{
//ss片选,mosi,scl配置为输出模式
gpio_config_t gpio_config_InitStructure;//配置spi 的结构体
gpio_config_InitStructure.intr_type=GPIO_INTR_DISABLE;//中断失能
gpio_config_InitStructure.mode=GPIO_MODE_OUTPUT;//配置为输出模式
gpio_config_InitStructure.pull_down_en=GPIO_PULLDOWN_DISABLE;//下拉失能
gpio_config_InitStructure.pull_up_en=GPIO_PULLUP_DISABLE;//上拉失能
gpio_config_InitStructure.pin_bit_mask=((1ull<<mySPI_SS_Pin) |(1ull<<mySPI_SCL_Pin) |(1ull<<mySPI_MOSI_Pin));//引脚掩码,配置为输出模式
gpio_config(&gpio_config_InitStructure);
//miso需要配置为输入模式
gpio_config_InitStructure.intr_type=GPIO_INTR_DISABLE;//中断失能
gpio_config_InitStructure.mode=GPIO_MODE_INPUT;//配置为输入模式
gpio_config_InitStructure.pull_down_en=GPIO_PULLDOWN_DISABLE;//下拉失能
gpio_config_InitStructure.pull_up_en=GPIO_PULLUP_ENABLE;//上拉使能
gpio_config_InitStructure.pin_bit_mask=1ull<<mySPI_MISO_Pin;//引脚掩码,配置为输入模式
gpio_config(&gpio_config_InitStructure);
ESP_LOGI(" init miso level is","%d",mySPI_Read_MISO());
//默认初始化引脚
mySPI_Write_SS(1);//,默认将片选置高电平,不选中从机
mySPI_Write_SCL(0);//spi模式0,SCK置低电平
}
/**
* @description: spi开始信号
* @return {无}
*/
void mySPI_Start()
{
mySPI_Write_SS(0);//拉低片选
}
/**
* @description: spi终止信号
* @return {无}
*/
void mySPI_Stop()
{
mySPI_Write_SS(1);//拉高片选
}
/**
* @description: spi主机和从机交换一个字节 ,使用SPI模式0
* @param {uint8_t} SendByte 要发送的一个字节
* @return {返回读取到的字节数据}
*/
uint8_t mySPI_SwapByte(uint8_t SendByte)
{
uint8_t receiveByte=0x00,i;
for(i=0;i<8;i++)
{
MySPI_Write_MOSI(SendByte & (0x80>>i));//发送一个字节,移出数据
mySPI_Write_SCL(1);//sck置高电平 ,第一个上升沿,移入数据,数据采样
if(mySPI_Read_MISO()==1)//主机负责将从机交换来的数据拿走
{
receiveByte|=(0x80>>i);
}
mySPI_Write_SCL(0);//拉低scl时钟信号
}
return receiveByte;
}
mySPI.h
#ifndef _MYSPI__H
#define _MYSPI__H
void mySPI_Init(void);
void mySPI_Start();
void mySPI_Stop();
uint8_t mySPI_SwapByte(uint8_t SendByte);
#endif
W25Q64.c
#include <stdint.h>
#include "W25Q64_Ins.h"
#include "mySPI.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终止
}
W25Q64.h
#ifndef __W25Q64_H__
#define __W25Q64_H__
void W25Q64_ReadID(uint8_t *MID ,uint16_t *DID);
void W25Q64_Init(void);
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_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
main.c
#include <esp_log.h>
#include "W25Q64.h"
#include <stdint.h>
uint8_t MID;//厂商号
uint16_t DID;//设备号
uint8_t ArrayWrite[]={0xA1,0xA2,0xA3,0xA4};
uint8_t ArrayRead[4];
void app_main(void)
{
/*模块初始化*/
W25Q64_Init(); //W25Q64初始化
/*显示ID号*/
W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号
ESP_LOGI("MID","%x",MID);//显示MID
ESP_LOGI("DID","%x",DID);//显示DID
/*W25Q64功能函数测试*/
W25Q64_SectorErase(0x000000); //扇区擦除
W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中
W25Q64_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中
/*显示数据*/
ESP_LOGI("ArrayWrite[0]","%x",ArrayWrite[0]);//显示写入数据的测试数组
ESP_LOGI("ArrayWrite[1]","%x",ArrayWrite[1]);
ESP_LOGI("ArrayWrite[2]","%x",ArrayWrite[2]);
ESP_LOGI("ArrayWrite[3]","%x",ArrayWrite[3]);
ESP_LOGI("ArrayRead[0]","%x",ArrayRead[0]);//显示读取数据的测试数组
ESP_LOGI("ArrayRead[1]","%x",ArrayRead[1]);
ESP_LOGI("ArrayRead[2]","%x",ArrayRead[2]);
ESP_LOGI("ArrayRead[3]","%x",ArrayRead[3]);
}
代码调试
目录结构
效果
------------------------------完结--------------------------