【嵌入式———SPI的基本操作——实验需求:案列1:软件模拟实现SPI协议和STM32的SPI模块实现(硬件实现)】

SPI(串行外围设备接口)

SPI接口主要应用于EEPROM,FLASH,各种传感器,AD转换器。

高速的,全双工,同步的串行通信总线。

物理层;

1个SPI有四根线
SCK :时钟信号线,用于通讯数据同步,它由通讯主机(MCU)产生,决定了通讯的速率。

不同的设备支持最高的时钟频率不一样,如:STM32的SPI时钟频率最大位fpclk/2
MOSI :主设备输出/从设备输入引脚
MISO :主设备输入/从设备输出引脚
SS :片选线或者使能线,也称NSS或者CS

如果主机要连接多条从设备就需要多条片选线

协议层:

1.来一个时钟上升沿信号,主机和从机分别把自己高位的值左移出来(一般高位先行)

2.来一个时钟下降沿信号,主机和从机分别读入数据,存储到移位寄存器的地位。

经过8次同样的操作,就完成了一个字节的交换。

时钟的极性和相位

CPOL:Clock Polarity ,通信的整个过程分为空闲时刻和通信时刻

空闲状态SCK是低电平,CPOL-0;上升沿表示数据有效的开始
CPHA:Clock Phase ,就是时钟的相位

直接决定SPI总线从那个跳变沿开始采样数据

CPHA=0:表示从第一个跳变沿开始采样

CPHA=1:表示从第二个跳变沿开始采样

所以SPI有四种不同的工作模式

W25Q32:使用SPI通讯协议的NOR FLASH存储器

硬件电路设计:


SPI软件模拟实现:

(1)W25Q32此芯片只支持模式0和模式3工作模式

(2)写的时候先擦除,再写入

(3)移位是高位优先

写入操作注意事项:

1.写入操作前,必须使能

2.每个数据位只能由1改写为0,不能由0改写为1(先擦除,此芯片段擦除)

3.连续写入多字节时,最多写入一页数据,超过页尾位置的数据,会回到页首覆盖写入

4.写入操作结束后,芯片进入忙状态,不响应新的读写状态(写周期内不响应其他的读写操作)
读取操作注意事项:

1.直接调用读取时序,无需使能

2读取操作不会进入忙状态,但不能在忙状态时读取

读写指令:

1.Write Enable(06h)

2.Page Program(02h/块号/段号/页号/页内地址/DATA)

3.Read Data(03h/读取地址)

4.Setcor Erase(4KB~段擦除)

5.Read State Register(05h)

读写时序图------模式0

在SPI.h中编写

c 复制代码
#ifndef __SPI_H
#define __SPI_H

#include "stm32f10x.h"
#include "delay.h"

// 宏定义,不同引脚输出高低电平
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)

#define SCK_HIGH (GPIOA->ODR |= GPIO_ODR_ODR5)
#define SCK_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR5)

#define MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)
#define MOSI_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR7)

// 读取MISO引脚信号
#define MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6)

// SPI标准延迟时间
#define SPI_DELAY Delay_us(5)

// 初始化
void SPI_Init(void);

// 数据传输的开始和结束
void SPI_Start(void);
void SPI_Stop(void);

// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte);

#endif

在SPI.c中编写

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

// 初始化
void SPI_Init(void)
{
    // 1. 开启时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    // 2. GPIO工作模式
    // PA5、PA7、PC13:通用推挽输出,CNF = 00,MODE = 11
    GPIOC->CRH |= GPIO_CRH_MODE13;
    GPIOC->CRH &= ~GPIO_CRH_CNF13;

    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL &= ~GPIO_CRL_CNF5;

    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL &= ~GPIO_CRL_CNF7;

    // PA6:MISO,浮空输入,CNF = 01,MODE = 00
    GPIOA->CRL &= ~GPIO_CRL_MODE6;
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOA->CRL |= GPIO_CRL_CNF6_0;

    // 3. 选择SPI的工作模式 0:SCK空闲0
    SCK_LOW;

    // 4. 片选不使能
    CS_HIGH;

    // 5. 延时
    SPI_DELAY;
}

// 数据传输的开始和结束
void SPI_Start(void)
{
    CS_LOW;
}
void SPI_Stop(void)
{
    CS_HIGH;
}

// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte)
{
    // 定义变量保存接收到的字节
    uint8_t rByte = 0x00;

    // 用一个循环,依次交换8位数据
    for (uint8_t i = 0; i < 8; i++)
    {
        // 1. 先准备要发送的数据(最高位),送到MOSI
        //取最高位
        if (byte & 0x80)
        {
            MOSI_HIGH;
        }
        else
        {
            MOSI_LOW;
        }

        // 左移一位
        byte <<= 1;

        // 2. 拉高时钟信号,形成一个上升沿
        SCK_HIGH;
        SPI_DELAY;

        // 3. 在MISO上采样Flash传来的数据
        // 先做左移
        rByte <<= 1;

        if (MISO_READ)
        {
            rByte |= 0x01;
        }

        // 4. 拉低时钟,为下次数据传输做准备
        SCK_LOW;
        SPI_DELAY;
    }

    return rByte;
}

FLASH接口函数

在W25Q32.h中编写

c 复制代码
#ifndef __W25Q32_H
#define __W25Q32_H

#include "spi.h"

// 初始化
void W25Q32_Init(void);

// 读取ID
void W25Q32_ReadID(uint8_t * mid, uint16_t * did);

// 开启写使能
void W25Q32_WriteEnable(void);

// 关闭写使能
void W25Q32_WriteDisable(void);

// 等待状态不为忙(busy)
void W25Q32_WaitNotBusy(void);

// 擦除段(sector erase),地址只需要块号和段号
void W25Q32_EraseSector(uint8_t block, uint8_t sector);

// 写入(页写)
void W25Q32_PageWrite(uint8_t block, uint8_t sector, uint8_t page, uint8_t * data, uint16_t len);

// 读取
void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page, uint8_t innerAddr, uint8_t * buffer, uint16_t len);

#endif

在W25Q32.c中编写

W25Q32地址组成:

c 复制代码
/*
 * @Author: wushengran
 * @Date: 2024-06-12 15:54:23
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */
#include "w25q32.h"

// 初始化
void W25Q32_Init(void)
{
    SPI_Init();
}

// 读取ID
void W25Q32_ReadID(uint8_t *mid, uint16_t *did)
{
    SPI_Start();

    // 1. 发送指令 9fh
    SPI_SwapByte(0x9f);

    // 2. 获取制造商ID(为了读取数据,发送什么不重要)
    *mid = SPI_SwapByte(0xff);

    // 3. 获取设备ID
    *did = 0;
    *did |= SPI_SwapByte(0xff) << 8;
    *did |= SPI_SwapByte(0xff) & 0xff;

    SPI_Stop();
}

// 开启写使能
void W25Q32_WriteEnable(void)
{
    SPI_Start();
    SPI_SwapByte(0x06);
    SPI_Stop();
}

// 关闭写使能
void W25Q32_WriteDisable(void)
{
    SPI_Start();
    SPI_SwapByte(0x04);
    SPI_Stop();
}

// 等待状态不为忙(busy)
void W25Q32_WaitNotBusy(void)
{
    SPI_Start();

    // 发送读取状态寄存器指令
    SPI_SwapByte(0x05);

    // 等待收到的数据末位变成0
    while (SPI_SwapByte(0xff) & 0x01)
    {
    }

    SPI_Stop();
}

// 擦除段(sector erase),地址只需要块号和段号
void W25Q32_EraseSector(uint8_t block, uint8_t sector)
{
    // 首先等待状态不为忙
    W25Q32_WaitNotBusy();

    // 开启写使能
    W25Q32_WriteEnable();

    // 计算要发送的地址(段首地址)
    uint32_t addr = (block << 16) + (sector << 12);

    SPI_Start();

    // 发送指令
    SPI_SwapByte(0x20);

    SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
    SPI_SwapByte(addr >> 8 & 0xff);  // 第二个字节
    SPI_SwapByte(addr >> 0 & 0xff);  // 第三个字节

    SPI_Stop();

    W25Q32_WriteDisable();
}

// 写入(页写)
void W25Q32_PageWrite(uint8_t block, uint8_t sector, uint8_t page, uint8_t *data, uint16_t len)
{
    // 首先等待状态不为忙
    W25Q32_WaitNotBusy();

    // 开启写使能
    W25Q32_WriteEnable();

    // 计算要发送的地址(页首地址)
    uint32_t addr = (block << 16) + (sector << 12) + (page << 8);

    SPI_Start();

    // 发送指令
    SPI_SwapByte(0x02);

    // 发送24位地址
    SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
    SPI_SwapByte(addr >> 8 & 0xff);  // 第二个字节
    SPI_SwapByte(addr >> 0 & 0xff);  // 第三个字节

    //  依次发送数据
    for (uint16_t i = 0; i < len; i++)
    {
        SPI_SwapByte(data[i]);
    }

    SPI_Stop();

    W25Q32_WriteDisable();
}

// 读取
void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page, uint8_t innerAddr, uint8_t *buffer, uint16_t len)
{
    // 首先等待状态不为忙
    W25Q32_WaitNotBusy();

    // 计算要发送的地址
    uint32_t addr = (block << 16) + (sector << 12) + (page << 8) + innerAddr;

    SPI_Start();

    // 发送指令
    SPI_SwapByte(0x03);

    // 发送24位地址
    SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
    SPI_SwapByte(addr >> 8 & 0xff);  // 第二个字节
    SPI_SwapByte(addr >> 0 & 0xff);  // 第三个字节

    //  依次读取数据
    for (uint16_t i = 0; i < len; i++)
    {
        buffer[i] = SPI_SwapByte(0xff);
    }

    SPI_Stop();
}

在main.c中编写

c 复制代码
#include "usart.h"
#include "w25q32.h"
#include <string.h>

int main(void)
{
	// 1. 初始化
	USART_Init();
	W25Q32_Init();

	printf("尚硅谷SPI软件模拟实验开始...\n");

	// 2. 读取ID进行测试
	uint8_t mid = 0;
	uint16_t did = 0;
	W25Q32_ReadID(&mid, &did);
	printf("mid = %#x, did = %#x\n", mid, did);

	// 3. 段擦除
	W25Q32_EraseSector(0, 0);

	// 4. 页写
	W25Q32_PageWrite(0, 0, 0, "12345678", 8);

	// 5. 读取
	uint8_t buffer[10] = {0};
	W25Q32_Read(0, 0, 0, 2, buffer, 6);

	printf("buffer = %s\n", buffer);

	while (1)
	{
	}
}

STM32的SPI外设实现代码:

外设总线频率的1/2

SPI1------APB2 SPI2,SPI3------APB1

支持4中外设模式

数据长度8,16位,可以高位先行和低位先行。

支持双线全双工,单线双向,单线模式

波特率发生器:用于生成通信的同步时钟

NSS片选信号,可以硬件控制,也可以软件控制(主设备模式时使用)

SPI案例2:SPI外设读写FLASH

相关寄存器及相关位:

SPI_CR2

SPI_CR1_MSTR,配置工作模式(1主设备)

SPI_CR1_BR_2:0

SPI_CR1_CP0L

SPI_CR1_CPHA

SPI_CR1_DDF(数据帧格式设置)

SPI_CR1_LSBfirst(低位先行)

SPI_CR1_SSM(软件从设备管理置于1)

SPI_CR1_SSI(使NSS引脚作用无效)

SPI_CR1_SPE(启动SPI)

SP1_SR_TXE

SPI_SR_RXNE

SPI_DR

在SPI.h中编写

c 复制代码
#ifndef __SPI_H
#define __SPI_H

#include "stm32f10x.h"

// 宏定义,不同引脚输出高低电平
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)

// 初始化
void SPI_Init(void);

// 数据传输的开始和结束
void SPI_Start(void);
void SPI_Stop(void);

// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte);

#endif

SPI.c中编写

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

// 初始化
void SPI_Init(void)
{
    // 1. 开启时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;

    // 2. GPIO工作模式
    // PC13:通用推挽输出,CNF = 00,MODE = 11
    GPIOC->CRH |= GPIO_CRH_MODE13;
    GPIOC->CRH &= ~GPIO_CRH_CNF13;

    // PA5、PA7: 复用推挽输出,CNF = 10,MODE = 11
    GPIOA->CRL |= GPIO_CRL_MODE5;
    GPIOA->CRL |= GPIO_CRL_CNF5_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF5_0;

    GPIOA->CRL |= GPIO_CRL_MODE7;
    GPIOA->CRL |= GPIO_CRL_CNF7_1;
    GPIOA->CRL &= ~GPIO_CRL_CNF7_0;

    // PA6:MISO,浮空输入,CNF = 01,MODE = 00
    GPIOA->CRL &= ~GPIO_CRL_MODE6;
    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
    GPIOA->CRL |= GPIO_CRL_CNF6_0;

    // 3. SPI相关配置
    // 3.1 配置SPI为主模式
    SPI1->CR1 |= SPI_CR1_MSTR;

    // 3.2 使用软件控制片选信号,直接拉高
    SPI1->CR1 |= SPI_CR1_SSM;
    SPI1->CR1 |= SPI_CR1_SSI;

    // 3.3 配置工作模式0,时钟极性和相位
    SPI1->CR1 &= ~SPI_CR1_CPOL;
    SPI1->CR1 &= ~SPI_CR1_CPHA;

    // 3.4 配置时钟分频系数,波特率选择:BR-001
    SPI1->CR1 &= ~SPI_CR1_BR;
    SPI1->CR1 |= SPI_CR1_BR_0;

    // 3.5 设置数据帧格式
    SPI1->CR1 &= ~SPI_CR1_DFF;

    // 3.6 配置高位先行MSB
    SPI1->CR1 &= ~SPI_CR1_LSBFIRST;

    // 3.7 SPI模块使能
    SPI1->CR1 |= SPI_CR1_SPE;
}

// 数据传输的开始和结束
void SPI_Start(void)
{
    CS_LOW;
}
void SPI_Stop(void)
{
    CS_HIGH;
}

// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte)
{
    // 1. 将要发送的数据byte写入发送缓冲区
    // 1.1 等待发送缓冲区为空
    while ((SPI1->SR & SPI_SR_TXE) == 0)
    {}
    
    // 1.2 将数据byte写入DR寄存器
    SPI1->DR = byte;

    // 2. 读取MISO发来的数据
    // 2.1 等待接收缓冲区为非空
    while ((SPI1->SR & SPI_SR_RXNE) == 0)
    {}
    // 2.1 从接收缓冲区读取数据,返回
    return (uint8_t)(SPI1->DR & 0xff);
}

在FLASH中代码不改变

在main.c中编写(无变化)

c 复制代码
/*
 * @Author: wushengran
 * @Date: 2024-05-23 15:14:48
 * @Description:
 *
 * Copyright (c) 2024 by atguigu, All Rights Reserved.
 */
#include "usart.h"
#include "w25q32.h"
#include <string.h>

int main(void)
{
	// 1. 初始化
	USART_Init();
	W25Q32_Init();

	printf("尚硅谷SPI软件模拟实验开始...\n");

	// 2. 读取ID进行测试
	uint8_t mid = 0;
	uint16_t did = 0;
	W25Q32_ReadID(&mid, &did);
	printf("mid = %#x, did = %#x\n", mid, did);

	// 3. 段擦除
	W25Q32_EraseSector(0, 0);

	// 4. 页写
	W25Q32_PageWrite(0, 0, 0, "12345678", 8);

	// 5. 读取
	uint8_t buffer[10] = {0};
	W25Q32_Read(0, 0, 0, 2, buffer, 6);

	printf("buffer = %s\n", buffer);

	while (1)
	{
	}
}
相关推荐
Miuney_MAX13 小时前
【单片机】之HC32F460中断向量选择
单片机·嵌入式硬件
XINVRY-FPGA16 小时前
XC3S1000-4FGG320I Xilinx AMD Spartan-3 SRAM-based FPGA
嵌入式硬件·机器学习·计算机视觉·fpga开发·硬件工程·dsp开发·fpga
猫猫的小茶馆19 小时前
【ARM】ARM的介绍
c语言·开发语言·arm开发·stm32·单片机·嵌入式硬件·物联网
猫猫的小茶馆19 小时前
【PCB工艺】数模电及射频电路基础
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·pcb工艺
点灯小铭19 小时前
基于单片机的智能药物盒设计与实现
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
梓德原19 小时前
【基础】详细分析带隙型稳压电路的工作原理
单片机·嵌入式硬件·物联网
国科安芯20 小时前
航天医疗领域AS32S601芯片的性能分析与适配性探讨
大数据·网络·人工智能·单片机·嵌入式硬件·fpga开发·性能优化
小李做物联网21 小时前
【物联网毕业设计】60.1基于单片机物联网嵌入式项目程序开发之图像厨房监测系统
stm32·单片机·嵌入式硬件·物联网
贝塔实验室1 天前
新手如何使用Altium Designer创建第一张原理图(三)
arm开发·单片机·嵌入式硬件·fpga开发·射频工程·基带工程·嵌入式实时数据库
@good_good_study1 天前
STM32 ADC多通道采样实验
stm32·单片机·嵌入式硬件