STM32之SPI读写W25Q128芯片

  1. SPI简介

    STM32的SPI是一个串行外设接口。它允许STM32微控制器与其他设备(如传感器、存储器等)进行高速、全双工、同步的串行通信。通常包含SCLK(串行时钟)、MOSI(主设备输出/从设备输入Master Output Slave Input)、MISO(主设备输入/从设备输出Master Input Slave Output)和NSS/CS片选信号Chip Select)这4条线,支持多个从设备连接到一个主设备上。

SPI,是一种高速的,全双工同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROMFLASH实时时钟AD转换器,还有数字信号处理器和数字信号解码器之间。

2. SPI使用步骤

我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128)为例,学习SPI。

2.1 SPI时钟SCLK

   SPI时钟特点主要包括:时钟速率时钟极性时钟相位三方面。

   时钟速率

SPI总线上的主设备必须在通信开始时候配置并生成相应的时钟信号。从理论上讲,只要实际可行,时钟速率就可以是你想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。

时钟极性

根据硬件制造商的命名规则不同,时钟极性通常写为CKPCPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。

CKP可以配置为1或0,这意味着可根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。须参考设备的数据手册才能正确设置CKPCKE

CKP = 0:时钟空闲IDLE为低电平0;

CKP = 1:时钟空闲IDLE为高电平1。

时钟相位

根据硬件制造商的不同,时钟相位通常写为CKECPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;

CKE = 0:在时钟信号SCK的第一个跳变沿采样

CKE = 1:在时钟信号SCK的第二个跳变沿采样

2.2四种操作根据

SPI的时钟极性时钟相位特性可以设置4不同的SPI通信操作模式,它们的区别是定义了在时钟脉冲的哪条边沿转换(toggles)输出信号,哪条边沿采样输入信号,还有时钟脉冲的稳定电平值(就是时钟信号无效时是高还是低),详情如下所示:

Mode0:CKP=0,CKE=0当空闲态时,SCK处于低电平,数据采样是在第1个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),(发送数据)数据发送是在下降沿。

Mode1:CKP=0,CKE=1当空闲态时,SCK处于低电平,数据发送是在第2个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。

Mode2:CKP=1,CKE=0当空闲态时,SCK处于高电平,数据采集是在第1个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。

Mode3:CKP=1,CKE=1当空闲态时,SCK处于高电平,数据发送是在第2个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

图中黑线采样数据的时刻蓝线SCK时钟信号

举个例子,下图是SPI Mode0读/写时序,可以看出SCK空闲状态为低电平,主机输出数据在第一个跳变沿被从机采样,主机输入数据同理。

3.STM32相关的SPI

    STM32的SPI外设可用作通讯的主机及从机, 支持最高的SCK时钟频率为fpclk/2 (STM32F103型号的芯片默认fpclk1为36MHz, fpclk2为72MHz),完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位, 可设置数据MSB先行或LSB先行。它还支持双线全双工、双线单向以及单线模式。 其中双线单向模式可以同时使用MOSI及MISO数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线, 当然这样速率会受到影响。我们只讲解双线全双工模式

3.1时钟控制逻辑

    时钟由寄存器控制。SCK线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的BR[0:2]位控制,该位是对fpclk时钟的分频因子, 对fpclk的分频结果就是SCK引脚的输出时钟频率,计算方法见表 BR位对fpclk的分频。

其中的fpclk频率是指SPI所在的APB总线频率, APB1为fpclk1,APB2为fpckl2。

通过配置“控制寄存器CR”的“CPOL位”及“CPHA”位可以把SPI设置成前面分析的4种SPI模式。

实际应用中,我们一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

3.2通讯过程

STM32使用SPI外设通讯时,在通讯的不同阶段它会对“状态寄存器SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。主发送器通讯过程中的是“主模式”流程,即STM32作为SPI通讯的主机端时的数据收发过程。

3.3从代码层面理解SPI

我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128),实现类似IIC的功能。

    使能 SPI2 的时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PORTB 时钟使能

RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE );//SPI2 时钟使能

GPIOB端口挂在STM32的APB2时钟线上。SPI2是挂在STM32的APB1时钟线上。

配置相关引脚的复用功能

这里使用 PB13、14、15 这 3 个(SCK.、MISO、MOSI,CS 使用软件管理方式),所以设置这三个为复用 IO。

GPIO_InitTypeDef GPIO_InitStructure;

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化 GPIOB

SPI2的引脚在PB上,可以参考W25Q128硬件连接图。

初始化 SPI2, 设置 SPI2 工作模式

库函数中是通过 SPI_Init 函数来实现。

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)

SPI_InitTypeDef 的定义:

typedef struct

{

uint16_t SPI_Direction;

uint16_t SPI_Mode;

uint16_t SPI_DataSize;

uint16_t SPI_CPOL;

uint16_t SPI_CPHA;

uint16_t SPI_NSS;

uint16_t SPI_BaudRatePrescaler;

uint16_t SPI_FirstBit;

uint16_t SPI_CRCPolynomial;

}SPI_InitTypeDef;

SPI_BaudRatePrescaler设置 SPI 波特率预分频值决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值

SPI_BaudRatePrescaler_256 //256 分频值

传输速度为 72M/256=281.25KHz。

初始化代码:

SPI_InitTypeDef SPI_InitStructure;

SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工

SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI

SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收 8 位帧结构

SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平

SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//第二个跳变沿数据被采样

SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由软件控制

SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频 256

SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始

SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式

SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器

使能SPI2

SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设

SPI2_ReadWriteByte(0xff); //④启动传输,主机发一个字节,进行一次传输,可以启动传输

SPI传输数据

发送数据函数

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)

接收数据函数

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)

查看 SPI 传输状态函数

判断数据是否传输完成,发送区是否为空
判断接收是否完成,接收区是否空

接收

SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE)

发送

SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE)

设置SPI2速度函数

单独的设置分频系数的函数

//SPI 速度设置函数

//SpeedSet://SPI_BaudRatePrescaler_256 256 分频 (SPI 281.25K@sys 72M)

void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)

{

    assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));

    SPI2->CR1&=0XFFC7;

    SPI2->CR1|=SPI_BaudRatePrescaler; //设置 SPI2 速度

    SPI_Cmd(SPI2,ENABLE);

}

读写一个字节

u8 SPI2_ReadWriteByte(u8 TxData)

{

    u8 retry=0;

    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //等待发送区空

    {

       retry++;//重试

       if(retry>200)return 0;

    } //读取两百次还没有值,说明无效,返回

    SPI_I2S_SendData(SPI2, TxData); //通过外设 SPIx 发送一个数据

    retry=0;

    while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //等待接收完一个 byte

    {

       retry++;

       if(retry>200)return 0;

    }

    return SPI_I2S_ReceiveData(SPI2); //返回通过 SPIx 最近接收的数据

}

W25Q128

• W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64等。

擦除

W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。
    这样要求芯片必须有 4K 以上 SRAM 才能很好的操作。

W25QXX驱动解读

W25QXX.h

W25QXX_CS片选,值0选定,1取消

初始化SPI

读取状态寄存器

写状态寄存器

擦除一个扇区

读取 SPI FLASH

//在指定地址开始读取指定长度的数据

//pBuffer:数据存储区

//ReadAddr:开始读取的地址(24bit)

//NumByteToRead:要读取的字节数(最大 65535)

void W25QXX_Read (u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)

{

u16 i;

SPI_FLASH_CS=0; //使能器件

SPI2_ReadWriteByte(W25X_ReadData); //发送读取命令

SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //发送 24bit 地址

SPI2_ReadWriteByte((u8)((ReadAddr)>>8));

SPI2_ReadWriteByte((u8)ReadAddr);

for(i=0;i<NumByteToRead;i++)

{

pBuffer[i]=SPI2_ReadWriteByte(0XFF); //循环读数

}

SPI_FLASH_CS=1;

}

无检查写函数

//在指定地址开始写入指定长度的数据,但是要确保地址不越界!

//pBuffer:数据存储区

//WriteAddr:开始写入的地址(24bit)

//NumByteToWrite:要写入的字节数(最大65535)

//CHECK OK

void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)  

{                    

    u16 pageremain;   

    pageremain=256-WriteAddr%256; //单页剩余的字节数              

    if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节,这也是结束标识

    while(1)

    {     

        W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);

        if(NumByteToWrite==pageremain)break;//写入结束了

        else //NumByteToWrite>pageremain

        {

            pBuffer+=pageremain;

            WriteAddr+=pageremain; 

            NumByteToWrite-=pageremain;          //减去已经写入了的字节数

            if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节

            else pageremain=NumByteToWrite;       //不够256个字节了

        }

        //按照页剩余写一次,然后256个字节的写,然后写最后一页多出来的。

    };     

}

NoCheck是说可以跨扇区的写
下方表示写了一个扇区

W25QXX_Write函数

作用与 W25QXX_Flash_Read 的作用类似,不过是用来写数据到 W25Q128 里面的,其代码如下:

u8 W25QXX_BUFFER[4096];

void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)

{

    u32 secpos;

    u16 secoff;

    u16 secremain;

    u16 i;

    u8 * W25QXX_BUF;

    W25QXX_BUF=W25QXX_BUFFER;

    secpos=WriteAddr/4096;//扇区地址,每个扇区是4096,所以除以4096得到的整数就是扇区的地址标号

    secoff=WriteAddr%4096;//在扇区内的偏移

    secremain=4096-secoff;//扇区剩余空间大小

    //printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用

    if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于 4096 个字节

    while(1)

    {

        W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容

        //secpos*4096是该扇区的起始地址

        for(i=0;i<secremain;i++)//校验数据

        {

            if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除,偏移地址内有数据

//擦除后的默认值是0xFFF

        }

        if(i<secremain)//需要擦除

        {

            W25QXX_Erase_Sector(secpos); //擦除这个扇区

            for(i=0;i<secremain;i++) //复制

            {

                W25QXX_BUF[i+secoff]=pBuffer[i];

            }

            W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区

        }

        else

            W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);

        //如果扇区剩余空间足够,直接写入扇区剩余区间.

        //是否需要写入下一个扇区

        if(NumByteToWrite==secremain)break;//写入结束了

        else//写入未结束

        {

            secpos++;//扇区地址增 1

            secoff=0;//偏移位置为 0

            pBuffer+=secremain; //指针偏移

            WriteAddr+=secremain; //写地址偏移

            NumByteToWrite-=secremain; //字节数递减

            if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完

            else secremain=NumByteToWrite; //下一个扇区可以写完了

        }

    };

}

//跟无检查页写入的逻辑一致。

该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的数据。

先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。

擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。

当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。

    我理解,整个STM32读写W25Q128是不断封装SPI_I2S_SendData和ReadData的一个过程,先封装到读写一个字节ReadWriteByte,再封装到读写一个扇区W25QXX_Write_NoCheck,W25QXX_Write。整个过程比较标准,无需太多改动。

4.STM32之SPI实战

    在实现STM32的SPI通讯之前,先做个小实验,实现USART串口通讯。因为调试STM32的SPI通讯可以把结果通过串口打印到电脑上显示,方便观察结果。

STM32CubeMX学习笔记(6)——USART串口使用_unused(huart)-CSDN博客

5.Keil调试代码

    今天学了Keil的debug功能,刚开始程序卡在了HAL_INIT这里,这是个很奇怪的问题。CubeMx生成的代码段,什么都没有做就是无法调试。原来要在CubeMx里面勾选一个调试功能。

在KEIL中勾选Use-MicroLib库

因为调用了printf,这是一个C++里面的功能,需要重映射。代码如下。

新建一个retarget.c文件。

#include "stdio.h"

#include "stm32f1xx_hal.h"

#include "usart.h"

#pragma import(__use_no_semihosting_swi)

#pragma import(__use_no_semihosting)

void _sys_exit(int x) {

    x = x;

}

struct __FILE  {

    int handle;

    /* Whatever you require here. If the only file you are using is */

    /* standard output using printf() for debugging, no file handling */

    /* is required. */

};

/* FILE is typedef’ d in stdio.h. */

FILE __stdout;

void _ttywrch(int ch){};

   

int fputc(int ch, FILE *f)

{

    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);

    return ch;

}

这段代码也是调了好久,网站上的或多或少不太对。

有了上面的代码,main函数就可以调用printf函数,打印到串口显示出来。

Stm32 debug停留在"BKPT 0xAB"或者"SWI 0xAB"的解决办法。

通过百度网盘分享的文件:SPI
链接:https://pan.baidu.com/s/1qzSFFV8-Vhrb0NzqbBAeCg?pwd=sshc 
提取码:sshc 

  • 18
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值