09:(寄存器开发)SPI的使用

SPI的使用

1、程序模拟SPI时序

实验:W25Q64模块的应用,此模块是基于SPI协议进行数据传输的,且具有掉电不丢失的特性,具体SPI和W25Q64的基础知识可以参考stm32标准库入门教程的第22/23章:链接: link

①SPI.c文件的代码如下:

c 复制代码
#include "SPI.h"

/**
 * PA4选择从机
 */
void SPI_NSS(uint8_t num)
{
    if(num == 0)
    {
        GPIOA->ODR &= ~GPIO_ODR_ODR4;//PA4引脚输出为0
    }else{
        GPIOA->ODR |= GPIO_ODR_ODR4;//PA4引脚输入为1
    }
}

/**
 * PA5时钟线引脚
 */
void SPI_SCL(uint8_t num)
{
    if(num == 0)
    {
        GPIOA->ODR &= ~GPIO_ODR_ODR5;//PA5引脚输出为0
    }else{
        GPIOA->ODR |= GPIO_ODR_ODR5;//PA5引脚输出为1
    }
    Delay_us(5);
}

/**
 * PA6从机输入引脚
 */
uint8_t SPI_Receive(void)
{
    uint8_t Bite;
    if((GPIOA->IDR & GPIO_ODR_ODR6) == 0)
    {
        Bite = 0;
    }else{
        Bite = 1;
    }
    return Bite;
}

/**
 * PA7主机输出引脚
 */
void SPI_Write(uint8_t num)
{
    if(num == 0)
    {
        GPIOA->ODR &= ~GPIO_ODR_ODR7;//PA7引脚输出为0
    }else{
        GPIOA->ODR |= GPIO_ODR_ODR7;//PA7引脚输出为1
    }
}

/**
 * SPI引脚的初始化:PA4 = NSS(从机选择低电平有效), PA5 = SCL(时钟线)
 * PA6 = MISO(从机输出主机输入), PA7 = MOSI(主机输出从机输入)
 */
void MySPI_Init(void)
{
    /* 1、开始时钟 */
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    /* 2、配置引脚模式:
                      PA4输出0/1,配置为通用推挽输出:MODE = 11,CNF = 00
                      PA5输出0/1,配置为通用推挽输出:MODE = 11,CNF = 00
                      PA6输入0/1,配置为浮空输入:    MODE = 00,CNF = 01
                      PA7输出0/1,配置为通用推挽输出:MODE = 11,CNF = 00 */
    GPIOA->CRL |= GPIO_CRL_MODE4;
    GPIOA->CRL &= ~GPIO_CRL_CNF4;//配置PA4引脚

    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL &= ~GPIO_CRL_CNF5;//配置PA5引脚

    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL &= ~GPIO_CRL_CNF7;//配置PA7引脚

    GPIOA->CRL &= ~GPIO_CRL_MODE6;
    GPIOA->CRL |= GPIO_CRL_CNF6_0;
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;//配置PA6引脚

    SPI_NSS(1);//默认先不选中从机
    SPI_SCL(0);//空闲为低电平
}

/**
 * 起始信号
 */
void MySPI_Start(void)
{
    SPI_NSS(0);
}

/**
 * 停止信号
 */
void MySPI_Stop(void)
{
    SPI_NSS(1);
}

/**
 * 发送数据和接收数据1个字节
 */
uint8_t MySPI_SendRecByte(uint8_t Byte)
{
    uint8_t Data = 0x00;
    for(uint8_t i = 0; i<8; i++)
    {
        SPI_Write(Byte & (0x80 >> i));//写入次高为数据
        SPI_SCL(1);//拉高SCL产生上升沿,让从机和主机读取输入
        if (SPI_Receive() != 0)//主句读取数据
        {
            Data |= (0x80 >> i);
        }
        SPI_SCL(0);//拉低SCL,为下一次上升沿做准备
    }
    return Data;
}

②SPI.h文件的代码如下:

c 复制代码
#ifndef __SPI_H
#define __SPI_H
#include "stm32f10x.h"
#include "Delay.h"

void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SendRecByte(uint8_t Byte);

#endif

③W25Q64.c文件的代码如下:

c 复制代码
#include "SPI_W25Q64.h"

/**
 * W25Q64Q初始化
 */
void W25Q64_Init(void)
{
    MySPI_Init();
}

/**
 * W25Q64写使能
 */
void W25Q64_WriteEnable(void)
{
    MySPI_Start();
    MySPI_SendRecByte(W25Q64_WRITE_ENABLE);//发送指令,写使能
    MySPI_Stop();
}

/**
 * W25Q64写失能
 */
void W25Q64_WriteDisable(void)
{
    MySPI_Start();
    MySPI_SendRecByte(W25Q64_WRITE_DISABLE);//发送指令,写失能
    MySPI_Stop();
}

/**
 * 读取模块的ID
 */
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
    MySPI_Start();
    MySPI_SendRecByte(W25Q64_JEDEC_ID);//发送指令,读取ID
    *MID = MySPI_SendRecByte(W25Q64_DUMMY_BYTE);//读取MID
    *DID = MySPI_SendRecByte(W25Q64_DUMMY_BYTE) << 8;//读取DID的低8位
    *DID |= MySPI_SendRecByte(W25Q64_DUMMY_BYTE);//读取DID的高8
    MySPI_Stop();
}

/**
 * 擦除指定的扇区:Block(0~127),Sector(0~15)
 */
void W25Q64_EraseSector(uint8_t Block,uint8_t Sector)
{
    /* 计数出,指定的块和扇区的首地址 */
    uint32_t Address =  Block * 0x010000 + Sector * 0x001000;

    /* 发送起始信号*/
    MySPI_Start();

    /* 发送指令 */
    MySPI_SendRecByte(W25Q64_SECTOR_ERASE_4KB);//写入擦除指令

    /* 发送需要被擦除的地址 */
    MySPI_SendRecByte(Address >> 16);//写入擦除的地址的高8位
    MySPI_SendRecByte((Address >> 8) & 0xff);//写入擦除的地址的次8位
    MySPI_SendRecByte(Address & 0xff);//写入擦除的地址的低8位

    /* 发送停止信号 */
    MySPI_Stop();
}

/**
 * 指定位置写入数据:
 * 块:Block(0~127)
 * 扇区:Sector(0~15)
 * 页:Page(0~15)
 * 发送的数据的字节数:Length(0~256)
 * 发送的数据数组:SendDatas
 */
void W25Q64_WriteBites(uint8_t Block,uint8_t Sector,
                       uint8_t Page,uint16_t Length,uint8_t SendDatas[])
{
    W25Q64_WriteEnable();//写使能,向往内存里面写入数据时要开启写使能
    
    /* 计数出指定位置的页首地址 */
    uint32_t ADDress = Block * 0x010000 + Sector * 0x001000 + Page * 0x000100;

    /* 发送起始信号*/
    MySPI_Start();

    /* 发送指令*/
    MySPI_SendRecByte(W25Q64_PAGE_PROGRAM);//指令:写入数据

    /* 写入数据 */
    MySPI_SendRecByte(ADDress >> 16);//发送高8位地址
    MySPI_SendRecByte((ADDress >> 8) & 0xff);//发送次8位地址
    MySPI_SendRecByte(ADDress & 0xff);//发送低8位地址
    for(uint16_t i = 0; i<Length; i++)
    {
        MySPI_SendRecByte(SendDatas[i]);
    }

    /* 发送停止信号*/
    MySPI_Stop();
}


/**
 * 指定位置读取数据:
 * 块:Block(0~127)
 * 扇区:Sector(0~15)
 * 页:Page(0~15)
 * 读取的数据的字节数:Length
 * 读取的数据数组:ReceiveDatas
 */
void W25Q64_ReadBites(uint8_t Block,uint8_t Sector,
                       uint8_t Page,uint16_t Length,uint8_t ReceiveDatas[])
{
    /* 计数出指定位置的页首地址 */
    uint32_t ADDress = Block * 0x10000 + Sector * 0x1000 + Page * 0x100;

    /* 发送起始信号*/
    MySPI_Start();

    /* 发送指令*/
    MySPI_SendRecByte(W25Q64_READ_DATA);//指令:读取数据

    /* 读取数据 */
    MySPI_SendRecByte(ADDress >> 16);//发送高8位地址
    MySPI_SendRecByte((ADDress >> 8) & 0xff);//发送次8位地址
    MySPI_SendRecByte(ADDress & 0xff);//发送低8位地址
    for(uint16_t i = 0; i<Length; i++)
    {
        ReceiveDatas[i] = MySPI_SendRecByte(W25Q64_DUMMY_BYTE);
    }

    /* 发送停止信号*/
    MySPI_Stop();
}

④W25Q64.h文件的代码如下:

c 复制代码
#ifndef __SPI_W25Q64_H
#define __SPI_W25Q64_H
#include "SPI.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 

void W25Q64_Init(void);
void W25Q64_WriteEnable(void);
void W25Q64_WriteDisable(void);
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID);
void W25Q64_EraseSector(uint8_t Block,uint8_t Sector);
void W25Q64_WriteBites(uint8_t Block,uint8_t Sector,
                       uint8_t Page,uint16_t Length,uint8_t SendDatas[]);
void W25Q64_ReadBites(uint8_t Block,uint8_t Sector,
                       uint8_t Page,uint16_t Length,uint8_t ReceiveDatas[]);

void W25Q64_Busy(void);

#endif

⑤主函数文件的代码如下:

c 复制代码
#include "stm32f10x.h"                
#include "OLED.h"
#include "SPI_W25Q64.h"
#include "stdio.h"


int main(void)
{
    uint8_t MID;
    uint16_t DID;
    
	OLED_Init();
    W25Q64_Init();
    OLED_Clear();
    
    OLED_ShowString(1,1,"MID:   DID:");

    W25Q64_ReadID(&MID,&DID);
    OLED_ShowHexNum(1,5,MID,2);
    OLED_ShowHexNum(1,13,DID,4);
    
    /* 实验模块的读写操作 */
    /* 1、写使能 */
    W25Q64_WriteEnable();

    /* 2、对需要保存的位置进行擦除 */
    W25Q64_EraseSector(0,0);//对第1块的第1个扇区进行擦除
    Delay_ms(50);

    /* 3、写入数据*/
    uint8_t *Data = "Hello World";
    W25Q64_WriteBites(0,0,0,11,Data);
    Delay_ms(50);

   /* 4、读取数据*/
    uint8_t Buff[256];
    W25Q64_ReadBites(0,0,0,11,Buff);

   /* 5、OLED显示出来*/
   OLED_ShowString(2,1,Buff);

	while(1)
	{
        
	}
}

实物效果如图所示:

2、硬件SPI的使用

①SPI.c文件的代码如下:

c 复制代码
#include "SPI.h"

/**
 * PA4手动进行选择从机
 */
void SPI_NSS(uint8_t num)
{
    if(num == 0)
    {
        GPIOA->ODR &= ~GPIO_ODR_ODR4;//PA4引脚输出为0
    }else{
        GPIOA->ODR |= GPIO_ODR_ODR4;//PA4引脚输入为1
    }
}

/**
 * 1、SPI引脚的初始化:PA4 = NSS(从机选择低电平有效), PA5 = SCL(时钟线)
 * PA6 = MISO(从机输出主机输入), PA7 = MOSI(主机输出从机输入)
 * 2、片上外设SPI的初始化:
 */
void MySPI_Init(void)
{
    /* 1、开始时钟:GPIOA,SPI1 */
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;

    /* 2、配置引脚模式:
                      PA4(手动)输出0/1,配置为通用推挽输出:MODE = 11,CNF = 00
                      PA5输出0/1,配置为复用推挽输出:MODE = 11,CNF = 10
                      PA6输入0/1,配置为浮空输入:    MODE = 00,CNF = 01
                      PA7输出0/1,配置为复用推挽输出:MODE = 11,CNF = 10 */
    GPIOA->CRL |= GPIO_CRL_MODE4;
    GPIOA->CRL &= ~GPIO_CRL_CNF4;//配置PA4引脚

    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL &= ~GPIO_CRL_CNF5_0;
    GPIOA->CRL |= GPIO_CRL_CNF5_1;//配置PA5引脚

    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL &= ~GPIO_CRL_CNF7_0;//配置PA7引脚
    GPIOA->CRL |= GPIO_CRL_CNF7_1;//配置PA7引脚

    GPIOA->CRL &= ~GPIO_CRL_MODE6;
    GPIOA->CRL |= GPIO_CRL_CNF6_0;
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;//配置PA6引脚

    /* 3、片上外设SPI的配置 */
    //默认8位数据帧,全双工,模式0,高位先行,禁用SSOE。这些都不用配置
    /* 3.1、将设备配置为主模式:SPI_CR1_MSTR = 1 */
    SPI1->CR1 |= SPI_CR1_MSTR;

    /* 3.2、波特率控制:SPI_CR1_BR[2:0] = 000(2分频)*/
    SPI1->CR1 &= ~SPI_CR1_BR;

    /* 3.3、启用软件从设备的管理(软件选择从设备):SPI_CR1_SSM = 1*/
    SPI1->CR1 |= SPI_CR1_SSM;
    SPI1->CR1 |= SPI_CR1_SSI;

    /* 3.4、SPI片上外设使能:SPI_CR1_SPE = 1 */
    SPI1->CR1 |= SPI_CR1_SPE;
    
    SPI_NSS(1);//默认先不选中从机
}

/**
 * 起始信号
 */
void MySPI_Start(void)
{
    SPI_NSS(0);
}

/**
 * 停止信号
 */
void MySPI_Stop(void)
{
    SPI_NSS(1);
}

/**
 * 发送数据和接收数据1个字节
 */
uint8_t MySPI_SendRecByte(uint8_t Byte)
{
    uint8_t Data = 0x00;
    /* 判断发送缓存区是否为空:SPI_SR_TXE = 1(空)*/
    while(!(SPI1->SR & SPI_SR_TXE));

    /* 发送缓存区写入数据 */
    SPI1->DR = Byte;

    /* 判断接收缓存区是否为非空:SPI->SR_RXNE = 1(非空)*/
    while (!(SPI1->SR & SPI_SR_RXNE));

    /* 接收缓存区接收数据 */ 
    Data = SPI1->DR;

    return Data;
}

②W25Q64.c文件的代码如下:

c 复制代码
#include "SPI_W25Q64.h"

/**
 * W25Q64Q初始化
 */
void W25Q64_Init(void)
{
    MySPI_Init();
}

/**
 * W25Q64写使能
 */
void W25Q64_WriteEnable(void)
{
    MySPI_Start();
    MySPI_SendRecByte(W25Q64_WRITE_ENABLE);//发送指令,写使能
    MySPI_Stop();
}

/**
 * W25Q64写失能
 */
void W25Q64_WriteDisable(void)
{
    MySPI_Start();
    MySPI_SendRecByte(W25Q64_WRITE_DISABLE);//发送指令,写失能
    MySPI_Stop();
}

/**
 * 读取模块的ID
 */
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
    MySPI_Start();
    MySPI_SendRecByte(W25Q64_JEDEC_ID);//发送指令,读取ID
    *MID = MySPI_SendRecByte(W25Q64_DUMMY_BYTE);//读取MID
    *DID = MySPI_SendRecByte(W25Q64_DUMMY_BYTE) << 8;//读取DID的低8位
    *DID |= MySPI_SendRecByte(W25Q64_DUMMY_BYTE);//读取DID的高8
    MySPI_Stop();
}

/**
 * 擦除指定的扇区:Block(0~127),Sector(0~15)
 */
void W25Q64_EraseSector(uint8_t Block,uint8_t Sector)
{
    /* 计数出,指定的块和扇区的首地址 */
    uint32_t Address =  Block * 0x010000 + Sector * 0x001000;

    /* 发送起始信号*/
    MySPI_Start();

    /* 发送指令 */
    MySPI_SendRecByte(W25Q64_SECTOR_ERASE_4KB);//写入擦除指令

    /* 发送需要被擦除的地址 */
    MySPI_SendRecByte(Address >> 16);//写入擦除的地址的高8位
    MySPI_SendRecByte((Address >> 8) & 0xff);//写入擦除的地址的次8位
    MySPI_SendRecByte(Address & 0xff);//写入擦除的地址的低8位

    /* 发送停止信号 */
    MySPI_Stop();
}

/**
 * 指定位置写入数据:
 * 块:Block(0~127)
 * 扇区:Sector(0~15)
 * 页:Page(0~15)
 * 发送的数据的字节数:Length(0~256)
 * 发送的数据数组:SendDatas
 */
void W25Q64_WriteBites(uint8_t Block,uint8_t Sector,
                       uint8_t Page,uint16_t Length,uint8_t SendDatas[])
{
    W25Q64_WriteEnable();//写使能,向往内存里面写入数据时要开启写使能
    
    /* 计数出指定位置的页首地址 */
    uint32_t ADDress = Block * 0x010000 + Sector * 0x001000 + Page * 0x000100;

    /* 发送起始信号*/
    MySPI_Start();

    /* 发送指令*/
    MySPI_SendRecByte(W25Q64_PAGE_PROGRAM);//指令:写入数据

    /* 写入数据 */
    MySPI_SendRecByte(ADDress >> 16);//发送高8位地址
    MySPI_SendRecByte((ADDress >> 8) & 0xff);//发送次8位地址
    MySPI_SendRecByte(ADDress & 0xff);//发送低8位地址
    for(uint16_t i = 0; i<Length; i++)
    {
        MySPI_SendRecByte(SendDatas[i]);
    }

    /* 发送停止信号*/
    MySPI_Stop();
}


/**
 * 指定位置读取数据:
 * 块:Block(0~127)
 * 扇区:Sector(0~15)
 * 页:Page(0~15)
 * 读取的数据的字节数:Length
 * 读取的数据数组:ReceiveDatas
 */
void W25Q64_ReadBites(uint8_t Block,uint8_t Sector,
                       uint8_t Page,uint16_t Length,uint8_t ReceiveDatas[])
{
    /* 计数出指定位置的页首地址 */
    uint32_t ADDress = Block * 0x10000 + Sector * 0x1000 + Page * 0x100;

    /* 发送起始信号*/
    MySPI_Start();

    /* 发送指令*/
    MySPI_SendRecByte(W25Q64_READ_DATA);//指令:读取数据

    /* 读取数据 */
    MySPI_SendRecByte(ADDress >> 16);//发送高8位地址
    MySPI_SendRecByte((ADDress >> 8) & 0xff);//发送次8位地址
    MySPI_SendRecByte(ADDress & 0xff);//发送低8位地址
    for(uint16_t i = 0; i<Length; i++)
    {
        ReceiveDatas[i] = MySPI_SendRecByte(W25Q64_DUMMY_BYTE);
    }

    /* 发送停止信号*/
    MySPI_Stop();
}

③主函数文件的代码如下:

c 复制代码
#include "stm32f10x.h"                
#include "OLED.h"
#include "SPI_W25Q64.h"
#include "stdio.h"


int main(void)
{
    uint8_t MID;
    uint16_t DID;
    
	OLED_Init();
    W25Q64_Init();
    OLED_Clear();
    
    OLED_ShowString(1,1,"MID:   DID:");

    W25Q64_ReadID(&MID,&DID);
    OLED_ShowHexNum(1,5,MID,2);
    OLED_ShowHexNum(1,13,DID,4);
    
    /* 实验模块的读写操作 */
    /* 1、写使能 */
    W25Q64_WriteEnable();

    // /* 2、对需要保存的位置进行擦除 */
    W25Q64_EraseSector(0,0);//对第1块的第1个扇区进行擦除
    Delay_ms(50);

    // /* 3、写入数据*/
     uint8_t *Data = "wo shi ni baba";
     W25Q64_WriteBites(0,0,0,14,Data);
     Delay_ms(50);

    // /* 4、读取数据*/
     uint8_t Buff[256];
     W25Q64_ReadBites(0,0,0,14,Buff);

    // /* 5、OLED显示出来*/
    OLED_ShowString(2,1,Buff);

	while(1)
	{
        
	}
}

实物效果如下图所示:

总结:

①操作单片机外的模块寄存器前,则需要对模块发送操作相关的指令。

②往W25Q64的内存里面写入数据之前要进行擦除和写使能

③软件模拟和硬件最大的区别在于:软件模拟发送数据是将数据的1位1位的写入到传输线上面(手动引脚的高低电平),通过手动模拟时钟信号来等待读取/写入数据。而硬件是将数据写入数据寄存器里面,配置好时钟,然后在时钟的作用下,片上外设自动的进行数据的发送与接收。

相关推荐
长潇若雪1 小时前
指针进阶(四)(C 语言)
c语言·开发语言·经验分享·1024程序员节
Gui林1 小时前
【GL07】C语言要点
c语言·算法
爱编程— 的小李2 小时前
有序序列合并(c语言)
c语言·算法
混迹网络的权某2 小时前
每天一道C语言精选编程题之求数字的每⼀位之和
c语言·开发语言·考研·算法·改行学it·1024程序员节
加载中loading...6 小时前
Linux线程安全(二)条件变量实现线程同步
linux·运维·服务器·c语言·1024程序员节
安科瑞刘鸿鹏6 小时前
校园建筑用电安全监测装置 电气火灾监测预防设备功能介绍
运维·服务器·网络·嵌入式硬件·安全·能源
Wx120不知道取啥名6 小时前
C语言之长整型有符号数与短整型有符号数转换
c语言·开发语言·单片机·mcu·算法·1024程序员节
Cici_ovo9 小时前
摄像头点击器常见问题——摄像头视窗打开慢
人工智能·单片机·嵌入式硬件·物联网·计算机视觉·硬件工程
无际单片机项目实战9 小时前
为什么STM32的HAL库那么难用,ST还是要硬推HAL库?
c语言·stm32·单片机·嵌入式硬件·物联网
小卡皮巴拉10 小时前
【力扣刷题实战】相同的树
c语言·算法·leetcode·二叉树·递归