【嵌入式———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)
	{
	}
}
相关推荐
小智学长 | 嵌入式1 小时前
单片机-STM32部分:10-2、逻辑分析仪
stm32·单片机·嵌入式硬件
美好的事情总会发生2 小时前
ROM详解
嵌入式硬件·硬件工程·智能硬件
金色光环3 小时前
1.stm32使用SSD1322 驱动3.12寸OLED
stm32·单片机·嵌入式硬件
腾飞的信仰10 小时前
51单片机同一个timer 作为定时器和波特率发生器么?
网络·单片机·51单片机
思考的味道13 小时前
系统的从零开始学习电子的相关知识,该如何规划?
嵌入式硬件
猿饵块15 小时前
STM32--PWM--函数
stm32·单片机·嵌入式硬件
学习噢学个屁15 小时前
基于51单片机步进电机控制—9个等级
c语言·单片机·嵌入式硬件·51单片机
GXSC17 小时前
国芯思辰| 医疗AED可使用2通道24位模拟前端SC2946(ADS1292)
嵌入式硬件
LaoZhangGong12317 小时前
分析rand()和srand()函数的功能
c语言·经验分享·stm32·单片机