STM32 SPI协议详解及应用指南

目录

1、SPI介绍

2、SPI的优点、缺点、特点        

3、SPI的物理架构

4、SPI的工作原理

5、SPI的工作模式

6、W25Q128介绍

7、实验:使用SPI通信读写W25Q128模块

cubeMX配置

代码实现

 效果展示

8、推荐去看的博客

1、SPI介绍

SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司推出的一 种同步串行接口技术,是一种高速的,全双工,同步的通信总线。

2、SPI的优点、缺点、特点        

SPI接口是在CPU和外围低速器件之间进行同步串行数据传输,在主器件的移位脉冲下,数据按位传输,低位在前高位在后,是全双工通信,数据的传输速度总体比I2C总线快,速度可以达到几Mbps。

优点:

  • 信号线少
  • 协议简单
  • 相对数据传输速率高。
  • 缺点:

  • 没有指定的流控制,流控制可以使数据接收设备在不能接收数据的时候通知发送设备,使其停止数据的发送。
  • 没有应答机制确认是否接收到数据,所以在数据可靠性上不如IIC协议
  • 特点:

  • 支持全双工、高速、同步、总线式
  • 主从机通信模式
  • 3、SPI的物理架构

     SPI 包含 4 条总线,SPI 总线包含 4 条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如下 :

  • SDO/MISO – Master Input Slave Output,主设备数据输入,从设备数据输出
  • SDI/MOSI – Master Output Slave Input,主设备数据输出,从设备数据输入
  • SCK – Serial Clock,时钟信号,由主设备产生
  • CS/SS – Chip Select,片选信号,由主设备控制。当有多个从设备时,因为每个从设备上都有一个片选引脚接入到主设备中,当主设备需要与某个从设备进行通信时就需要将从设备对应的片选引脚拉低或拉高
  • 4、SPI的工作原理

    在这里可以看出spi通信时,显现出一个环形结构 ,当主机发送一个长度的数据给从机的时候,从机也会相应的发送一个长度的数据给主机

    说明:SPI只有主模式和从模式之分,没有读写的说法,外设的写操作和读操作是同步完成的。若只有写操作,主机只需忽略接收到的字节(虚拟数据);反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收到一个数据必须也要先发一个数据。

    5、SPI的工作模式

    时钟极性(CPOL):

    没有数据传输时时钟线的空闲状态电平

  • 0:SCK在空闲状态保持低电平
  • 1:SCK在空闲状态保持高电平
  • 时钟相位(CPHA):

    时钟线在第几个时钟边沿采样数据

  • 0:SCK的第一(奇数)边沿进行数据采样,数据在第一个时钟边沿被锁存
  • 1:SCK的第二(偶数)边沿进行数据采样,数据在第二个时钟边沿被锁存
  • 模式0和模式3最常用

    模式0时序图:

     模式3时序图:

    需要注意的是:我们的主设备能够控制时钟(CS),因为我们的SPI通信并不像UART或者IIC通信那样有专门的通信周期,有专门的通信起始信号,有专门的通信结束信号;所以我们的SPI协议能够通过控制时钟信号线,当没有数据交流的时候我们的时钟线要么是保持高电平要么是保持低电平

    6、W25Q128介绍

    1.什么是W25Q128?

  • W25Q128 是华邦公司推出的一款 SPI 接口的 NOR Flash 芯片,其存储空间为 128 Mbit,相当于 16M 字节。
  • Flash 是常用的用于储存数据的半导体器件,它具有容量大,可重复擦写、按“扇区/块”擦除、掉电后数据可 继续保存的特性。
  • Flash 是有一个物理特性:只能写 0 ,不能写 1 ,写 1 靠擦除。
  • 2.W25Q128存储架构

  • 一般按扇区(4k)进行擦除。
  • 块 — 扇 — 页 — 字节
  •  3.W25Q128常用指令

    W25Q128
    全部指令非常多,但常用的如下几个指令:

    写使能(06H)

    执行页写,扇区擦除,块擦除,片擦除,写状态寄存器等指令前,需要写使能。

    拉低
    CS
    片选

    发送
    06H

    拉高
    CS
    片选

    读状态寄存器(05H)

    拉低CS片选 → 发送05H→ 返回SR1的值 → 拉高CS片选

    读时序(03H)

    拉低
    CS
    片选

    发送
    03H

    发送
    24
    位地址

    读取数据(
    1~n


    拉高
    CS
    片选

    页写时序(02H)

    页写命令最多可以向
    FLASH
    传输
    256
    个字节的数据。

    拉低
    CS
    片选

    发送
    02H

    发送
    24
    位地址

    发送数据(
    1~n


    拉高
    CS
    片选

    扇区擦除时序(20H)

    写入数据前,检查内存空间是否全部都是
    0XFF
    ,不满足需擦除。

    拉低
    CS
    片选

    发送
    20H

    发送
    24
    位地址

    拉高
    CS
    片选

    4.W25Q128状态寄存器

    W25Q128
    一共有
    3
    个状态寄存器,它们的作用是跟踪芯片的状态。 其中,状态寄存器 1
    较为常用。

    BUSY
    :指示当前的状态,
    0
    表示空闲,
    1
    表示忙碌

    WEL
    :写使能锁定,为
    1
    时,可以操作页
    /
    扇区
    /
    块。为
    0
    时,写禁止。

    5.W25Q128常见操作

    读操作:拉低CS片选 → 发送读命令(03H)→ 发送24位地址 → 读取数据→拉高CS片选

    擦除扇区:拉低CS片选 → 发送写使能命令(06H)→ 等待空闲→ 发送擦除扇区命令(20H)→ 发送24位地址 → 等待空闲→拉高CS片选

    写操作:

    7、实验:使用SPI通信读写W25Q128模块

    实验内容:使用SPI通信输入数据给W25Q128模块,再读取模块内刚刚输入进去的数据,最后通过串口输出

    cubeMX配置

    代码实现

    用到的库函数

    HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,

                                                                                    uint8_t *pTxData,

                                                                                    uint8_t *pRxData,                                                                                                                      uint16_t Size,
                                                                                    uint32_t Timeout)

    参数一:SPI_HandleTypeDef *hspi,spi句柄

    参数二:uint8_t *pTxData,待主机输入的数据的指针(指向传输数据缓冲区的指针)

    参数三:uint8_t *pRxData, 待从机输出的数据的指针(指向接收数据缓冲区的指针)

    参数四:uint16_t Size,传输数据的长度

    参数五:uint32_t Timeout,设置超时时间

    返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)

    需要修改三个文件,需要添加两个文件

    w25q128.c

    #include "w25q128.h"
    #include "spi.h"
    #include "stdio.h"
    
    /**
     * @brief       初始化W25Q128
     * @param       无
     * @retval      无
     */
    void w25q128_init(void)//已看
    {
    		uint16_t flash_type;//芯片id
        //spi1_read_write_byte(0xFF); /* 清除DR的作用,DR数据寄存器*/这里其实可以不用		
        W25Q128_CS(1);//拉高不选中
    		flash_type = w25q128_read_id();   /* 读取FLASH ID. */
    		if (flash_type == W25Q128)//芯片id16位站两个字节
    			printf("检测到W25Q128芯片\r\n");
    }
    
    /**
     * @brief       等待空闲
     * @param       无
     * @retval      无
     */
    static void w25q128_wait_busy(void)//已看
    {
        while ((w25q128_rd_sr1() & 0x01) == 0x01);   /* 等待BUSY位清空 */
    }
    
    /**
     * @brief       读取W25Q128的状态寄存器1的值
     * @param       无
     * @retval      状态寄存器值
     */
    uint8_t w25q128_rd_sr1(void)//已看
    {
        uint8_t rec_data = 0;
        
        W25Q128_CS(0);
        spi1_read_write_byte(FLASH_ReadStatusReg1);     /* 读状态寄存器1 */
        rec_data = spi1_read_write_byte(0xFF);
        W25Q128_CS(1);
        
        return rec_data;
    }
    
    /**
     * @brief       W25Q128写使能
     *   @note      将S1寄存器的WEL置位
     * @param       无
     * @retval      无
     */
    void w25q128_write_enable(void)//已看
    {
        W25Q128_CS(0);
        spi1_read_write_byte(FLASH_WriteEnable);   /* 发送写使能 */
        W25Q128_CS(1);
    }
    
    /**
     * @brief       W25Q128发送地址
     * @param       address : 要发送的地址
     * @retval      无
     */
    static void w25q128_send_address(uint32_t address)//已看先发高位再发低位,单位发送的时候是由低往高走的
    {
        spi1_read_write_byte((uint8_t)((address)>>16));     /* 发送 bit23 ~ bit16 地址 */
        spi1_read_write_byte((uint8_t)((address)>>8));      /* 发送 bit15 ~ bit8  地址 */
        spi1_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
    }
    
    /**
     * @brief       擦除整个芯片
     *   @note      等待时间超长...
     * @param       无
     * @retval      无
     */
    void w25q128_erase_chip(void)
    {
        w25q128_write_enable();    /* 写使能 */
        w25q128_wait_busy();       /* 等待空闲 */
        W25Q128_CS(0);
        spi1_read_write_byte(FLASH_ChipErase);  /* 发送读寄存器命令 */ 
        W25Q128_CS(1);
        w25q128_wait_busy();       /* 等待芯片擦除结束 ,空闲了就说明擦除结束了*/
    }
    
    /**
     * @brief       擦除一个扇区
     *   @note      注意,这里是扇区地址,不是字节地址!!
     *              擦除一个扇区的最少时间:150ms
     *
     * @param       saddr : 扇区地址 根据实际容量设置
     * @retval      无
     */
    void w25q128_erase_sector(uint32_t saddr)//已看
    {
        //printf("fe:%x\r\n", saddr);   /* 监视falsh擦除情况,测试用 */
        saddr *= 4096;//一个扇区有4096字节,saddr告诉我们第几个扇区*4096计算出对应的首地址
        w25q128_write_enable();        /* 写使能 */
        w25q128_wait_busy();           /* 等待空闲 */
    
        W25Q128_CS(0);
        spi1_read_write_byte(FLASH_SectorErase);    /* 发送写页命令,不是页写命令 ,发送的是扇区擦除命令*/
        w25q128_send_address(saddr);   /* 发送地址 */
        W25Q128_CS(1);
        w25q128_wait_busy();           /* 等待扇区擦除完成 */
    }
    
    /**
     * @brief       读取芯片ID
     * @param       无
     * @retval      FLASH芯片ID
     *   @note      芯片ID列表见: w25q128.h, 芯片列表部分
     */
    uint16_t w25q128_read_id(void)//已看
    {
        uint16_t deviceid;
    
        W25Q128_CS(0);//设备片选,使能
        spi1_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读 ID 命令 */
        spi1_read_write_byte(0);    /* 写入一个字节 */
        spi1_read_write_byte(0);
        spi1_read_write_byte(0);		//发送读取指令之后需要发送000000h24位的bit地址信号来启动命令
        deviceid = spi1_read_write_byte(0xFF) << 8;     /* 读取高8位字节 */
        deviceid |= spi1_read_write_byte(0xFF);         /* 读取低8位字节 */
        W25Q128_CS(1);//拉高片选
        return deviceid;
    }
    
    /**
     * @brief       读取SPI FLASH
     *   @note      在指定地址开始读取指定长度的数据
     * @param       pbuf    : 数据存储区
     * @param       addr    : 开始读取的地址(错误:最大32bit,最大是24bit)
     * @param       datalen : 要读取的字节数(最大65535,一块的数据容量)
     * @retval      无
     */
    void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)//已看
    {
        uint16_t i;
    
        W25Q128_CS(0);
        spi1_read_write_byte(FLASH_ReadData);       /* 发送读取命令 */
        w25q128_send_address(addr);                /* 发送地址 */
        
        for(i=0;i<datalen;i++)
        {
            pbuf[i] = spi1_read_write_byte(0XFF);   /* 循环读取 */
        }
        
        W25Q128_CS(1);
    }
    
    /**
     * @brief       SPI在一页(0~65535这是块的范围,意思在这个快的某一页)内写入少于256个字节的数据
     *   @note      在指定地址开始写入最大256字节的数据
     * @param       pbuf    : 数据存储区
     * @param       addr    : 开始写入的地址(最大24bit)
     * @param       datalen : 要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
     * @retval      无
     */
    static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)//已看
    {
        uint16_t i;
    
        w25q128_write_enable();    /* 写使能 */
    
        W25Q128_CS(0);/*拉高*/
        spi1_read_write_byte(FLASH_PageProgram);    /* 发送写页命令 */
        w25q128_send_address(addr);                /* 发送地址 */
    
        for(i=0;i<datalen;i++)
        {
            spi1_read_write_byte(pbuf[i]);          /* 循环写入 */
        }
        
        W25Q128_CS(1);/*拉低*/
        w25q128_wait_busy();       /* 等待写入结束 */
    }
    
    /**
     * @brief       无检验写SPI FLASH(不带擦除,可以以下面那个函数做对比)
     *   @note      必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
     *              具有自动换页功能
     *              在指定地址开始写入指定长度的数据,但是要确保地址不越界!
     *							写入的数据可以大于256个字节
     *
     * @param       pbuf    : 数据存储区(要写入的数据)
     * @param       addr    : 开始写入的地址(最大32bit)
     * @param       datalen : 要写入的字节数(最大65535)
     * @retval      无
     */
    static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)//已看
    {
        uint16_t pageremain;
        pageremain = 256 - addr % 256;  /* 单页剩余的字节数 */
    
        if (datalen <= pageremain)      /* 不大于256个字节 */
        {
            pageremain = datalen;
        }
    
        while (1)
        {
            /* 当写入字节比页内剩余地址还少的时候, 一次性写完
             * 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理
             */
            w25q128_write_page(pbuf, addr, pageremain);
    
            if (datalen == pageremain)   /* 写入结束了 */
            {
                break;
            }
            else     /* datalen > pageremain */
            {
                pbuf += pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 ,他的指针是指向数组的首地址*/
                addr += pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */
                datalen -= pageremain;      /* 写入总长度减去已经写入了的字节数 */
    
                if (datalen > 256)          /* 剩余数据还大于一页,可以一次写一页 */
                {
                    pageremain = 256;       /* 一次可以写入256个字节 */
                }
                else     /* 剩余数据小于一页,可以一次写完 */
                {
                    pageremain = datalen;   /* 不够256个字节了 */
                }
            }
        }
    }
    
    /**
     * @brief       写SPI FLASH
     *   @note      在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
     *              SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block
     *              擦除的最小单位为Sector.
     *
     * @param       pbuf    : 数据存储区
     * @param       addr    : 开始写入的地址(最大32bit)
     * @param       datalen : 要写入的字节数(最大65535)
     * @retval      无
     */
    uint8_t g_w25q128_buf[4096];   /* 扇区缓存 */
    //块--扇--页--字节
    void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
    {
        uint32_t secpos;
        uint16_t secoff;
        uint16_t secremain;
        uint16_t i;
        uint8_t *w25q128_buf;//可有可无,去了就得改代码
    
        w25q128_buf = g_w25q128_buf;//定义了字符串指针的内存空间,可有可无
        secpos = addr / 4096;       /* 扇区地址,一个扇区占4096个字节,一个字节就是一个地址位 */
        secoff = addr % 4096;       /* 在扇区内的偏移,判断在这个扇区的那个位置写入 */
        secremain = 4096 - secoff;  /* 扇区剩余空间大小 */
    
        //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
        if (datalen <= secremain)
        {
            secremain = datalen;    /* 不大于4096个字节,写入的数据不大于扇区剩余的空间大小*/
        }
    
        while (1)
        {
            w25q128_read(w25q128_buf, secpos * 4096, 4096);   /* 读出整个扇区的内容 */
    
            for (i = 0; i < secremain; i++)   /* 校验数据 */
            {
                if (w25q128_buf[secoff + i] != 0XFF)/*判断扇区内是否有数据*/
                {
                    break;      /* 需要擦除, 直接退出for循环*/
                }
            }
    
            if (i < secremain)   /* 需要擦除,其实也就是上面for循环没结束之前就跳出循环了就说明我们写入数据的地方本来有数据需要我们擦除*/
            {
                w25q128_erase_sector(secpos);  /* 擦除这个扇区 */
    
                for (i = 0; i < secremain; i++)   /* 复制,将我们需要写入的数据复制我们要写入位置的前面数据的后面*/
                {
                    w25q128_buf[i + secoff] = pbuf[i];
                }
    
                w25q128_write_nocheck(w25q128_buf, secpos * 4096, 4096);  /* 写入整个扇区 */
            }
            else        /* 写已经擦除了的,直接写入扇区剩余区间.写入数据的位置没有数据就不需要我们擦除直接写入即可 */
            {
                w25q128_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
            }
    
            if (datalen == secremain)
            {
                break;  /* 写入结束了 */
            }
            else        /* 写入未结束 */
            {
                secpos++;               /* 扇区地址增1 */
                secoff = 0;             /* 偏移位置为0 */
    
                pbuf += secremain;      /* 指针偏移 */
                addr += secremain;      /* 写地址偏移 */
                datalen -= secremain;   /* 字节数递减 */
    
                if (datalen > 4096)
                {
                    secremain = 4096;   /* 下一个扇区还是写不完 */
                }
                else
                {
                    secremain = datalen;/* 下一个扇区可以写完了 */
                }
            }
        }
    }
    
    

    w25q128.h

    #ifndef __W25Q128_H__
    #define __W25Q128_H__
    
    #include "stdint.h"
    
    /* W25Q128片选引脚定义 */
    #define W25Q128_CS_GPIO_PORT           GPIOA
    #define W25Q128_CS_GPIO_PIN            GPIO_PIN_4
    
    /* W25Q128片选信号 */
    #define W25Q128_CS(x)      do{ x ? \
                                      HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_SET) : \
                                      HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_RESET); \
                                }while(0)
    
    /* FLASH芯片列表 */
    #define W25Q128     0XEF17          /* W25Q128  芯片ID */
    
    /* 指令表 */
    #define FLASH_WriteEnable						0x06 //写使能
    #define FLASH_ReadStatusReg1				0x05 //读SR1读取状态寄存器的值
    #define FLASH_ReadData							0x03 //读数据
    #define FLASH_PageProgram						0x02 //页写
    #define FLASH_SectorErase						0x20 //扇区擦除
    #define FLASH_ChipErase							0xC7 
    #define FLASH_ManufactDeviceID			0x90 //获取芯片id
    
    /* 静态函数 */
    static void w25q128_wait_busy(void);               /* 等待空闲 */
    static void w25q128_send_address(uint32_t address);/* 发送地址 */
    static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入page */
    static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写flash,不带擦除 */
    
    /* 普通函数 */
    void w25q128_init(void);                   /* 初始化25QXX */
    uint16_t w25q128_read_id(void);            /* 读取FLASH ID */
    void w25q128_write_enable(void);           /* 写使能 */
    uint8_t w25q128_rd_sr1(void);							/* 读取寄存器1的值 */
    
    void w25q128_erase_chip(void);             /* 整片擦除 */
    void w25q128_erase_sector(uint32_t saddr); /* 扇区擦除 */
    void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);     /* 读取flash */
    void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入flash */
    
    #endif
    

    spi.c(在这里添加读写一个字节的函数)

    uint8_t spi1_read_write_byte(uint8_t data)
    {
        uint8_t rec_data = 0;
    		//发送和接收一个字节
        HAL_SPI_TransmitReceive(&hspi1, &data, &rec_data, 1, 1000);
        
        return rec_data;//返回接收的字节
    }

    usart.c(对printf进行重构)记得要勾上Use MicroLIB

    int fputc(int ch, FILE *f)
    {      
        unsigned char temp[1]={ch};
        HAL_UART_Transmit(&huart1,temp,1,0xffff);  
        return ch;
    }

    main.c

    //前面需要引用对应的头文件
    //main函数外面
    #define TEXT_SIZE 16
    #define  FLASH_WriteAddress     0x00000
    #define  FLASH_ReadAddress      FLASH_WriteAddress
    
    //mian函数里面
    	uint8_t datatemp[TEXT_SIZE];//需要发送的数据
    	w25q128_init();//模块初始化
    	
    	/* 写入测试数据 */
    	sprintf((char *)datatemp, "liangxu shuai");//sprintf格式化输出到datatemp
    	w25q128_write(datatemp, FLASH_WriteAddress, TEXT_SIZE);//写入liangxu shuai,FLASH_WriteAddress=0x000000
    	printf("数据写入完成!\r\n");
    	
    	/* 读出测试数据 */
    	memset(datatemp, 0, TEXT_SIZE);//清空数据源
    	w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE);//读取我们刚刚写进去的数据FLASH_ReadAddress=0x000000
    	printf("读出数据:%s\r\n", datatemp);//在串口输出

     效果展示

    8、推荐去看的博客 

    一文搞懂SPI通信协议

    SPI通信协议(SPI总线)学习

    SPI、I2C、UART三种串行总线的原理、区别及应用

    SPI学习(二):SPI工作机制与协议解析  

    关于读的时候为什么要发送0xff问题

    SPI写入数据的时候记得读取,不然会一直读出0xFF!

    使用spi协议,接收来自slave的数据之前写0xff的原因

    为什么SPI在读数据时要写入一个0xff先

     写在最后:文章有向其他文章转载的内容,如有侵权请联系,可以马上删除

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 SPI协议详解及应用指南

    发表评论