stm32学习笔记-11 SPI通信

news2024/11/20 13:21:02

11 SPI通信

文章目录

  • 11 SPI通信
    • 11.1 SPI通信协议
    • 11.2 W25Q64简介
    • 11.3 实验:软件SPI读写W25Q64
    • 11.4 SPI通信外设
    • 11.5 实验:硬件SPI读写W25Q64

注:笔记主要参考B站 江科大自化协 教学视频“ STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的 Github仓库。


11.1 SPI通信协议

图11-1 使用到SPI协议的器件

SPI(Serial Peripheral Interface,串行外设接口)是由Motorola公司开发的一种通用数据总线,与IIC差不多,也是为了实现主控芯片和各种外挂芯片之间的数据交流。SPI和IIC都是常用的接口协议,只是根据其不同的特点,应用场景有所不同。

  • 四根通信线(SPI官方文档名称):
  1. SCK:Serial Clock,串行时钟线。别称SCLK、CLK、CK。作用是提供时钟信号,数据位的输入和输出都是在时钟上升沿、下降沿进行的。
  2. MOSI:Master Output Slave Input,主机输出从机输入。别称DO(Data Output)。
  3. MISO:Master Input Slave Output,主机输入从机输出。别称DI(Data Input)。
  4. SS:Slave Select,从机选择。别称NSS(Not Slave Select)、CS(Chip Select)。SPI协议可以为每个从机都开辟一条SS线,专门用于控制该从机的选择(低电平有效)(SPI壕无人性)。
  • 同步(时钟线快点慢点无所谓),全双工,高位先行
  • 支持总线挂载多设备,仅支持“一主多从”,不支持“多主机”。

IIC和SPI各有优缺点。IIC通过各种软硬件设置,使用最少的硬件资源(2根通信线)实现了最多的功能(双向通信、应答位等),相当于一个“精打细算、思维灵活”的协议;但是为了实现这些功能,其硬件采用开漏输出+上拉电阻的模式以防止电源短路,导致其高电平驱动能力不足,同时也导致其上升沿时间很长,这限制了其最高通信速率(IIC标准模式100kHz/快速模式400kHz)。相对的,SPI协议并没有规定最大传输速度,而是取决于外挂芯片厂商的设计需求,比如W25Q64芯片手册说明其SPI速率最高可达80MHz(甚至比stm32主频还要高)!但是,SPI的设计简单粗暴(学习起来更加简单),功能也不如IIC多,并且会消耗4根通信引脚,所以SPI相当于出手阔绰的土豪(我不在乎占了几根通信线,我只在乎我的任务有没有最简单、最快速的完成)。

注:IIC通过改进电路的方式,设计出高速模式3.4MHz,但目前并不普及。一般仍认为最高速率400kHz。

下面来介绍SPI的硬件规定

图11-2 典型的SPI应用电路图
  • SPI主机:主机一般是控制器,如stm32。
  • SPI从机:从机一般是存储器、显示屏、通信模块、传感器等。
  • 时钟线和数据线:所有SPI设备的SCK、MOSI、MISO分别连在一起,即相同名称的管脚连接在一起
  • 片选线:主机另外引出多条SS控制线,分别接到各从机的SS引脚。注意主机在同一时间只能选择一个从机进行通信(低电平有效),否则会造成数据冲突。

注:SPI输出引脚配置为推挽输出(驱动能力强),输入引脚配置为浮空或上拉输入。对于从机来说,只有当其被选中时,MISO才配置为推挽输出,否则为高阻态。

下面来介绍SPI基本收发时序
SPI通信的基础是交换字节。也就是说,每次SPI通信的过程中,通过各自的MOSI、MISO线,主机和从机的寄存器会形成一个循环移位操作,每个比特的通信都是转圈的循环移位,8个时钟周期完整的交换一个字节。那么根据需求有选择的忽略交换过来的数据,就可以实现(以主机举例,从机同理)主机只发送、主机只接收、主从机交换数据这三类操作。

图11-3 SPI基本收发电路——移位示意图

工作原理:

  • 波特率发生器上升沿:所有寄存器左移一位。
  • 波特率发生器下降沿:将采样输入的数据放到寄存器的最低位。
  • 重复8个时钟周期,便可以实现主机和从机的数据交换。

注:实际上,何时移位、何时采样、时钟极性都是可以设置的,下面将介绍。

功能介绍:显然存在资源浪费现象。

  • 同时进行发送和接收:正常的交换字节。
  • 只想发送、不想接收:不看接收过来的数据。
  • 只想接收、不想发送:随便发一个数据,比如0x00/0xFF。

下面介绍SPI交换单个字节的时序

  • 起始条件和终止条件:起始条件是SS从高电平切换到低电平,终止条件是SS从低电平切换到高电平。
  • 交换一个字节:两个配置位分别为CPOL(Clock Polarity, 时钟极性)规定空闲状态的时钟高低电平、CPHA(Clock Phase, 时钟相位)规定数据移入(数据采样)、移出的时机。
  1. 【模式0】[CPOL,CPHA] = [0,0],SCK低电平为空闲状态;SCK第一个边沿(上升沿)移入数据,第二个边沿(下降沿)移出数据。
  2. 【模式1】[CPOL,CPHA] = [0,1],SCK低电平为空闲状态;SCK第一个边沿移出数据,第二个边沿移入数据。
  3. 【模式2】[CPOL,CPHA] = [1,0],SCK高电平为空闲状态;SCK第一个边沿移入数据,第二个边沿移出数据。
  4. 【模式3】[CPOL,CPHA] = [1,1],SCK高电平为空闲状态;SCK第一个边沿移出数据,第二个边沿移入数据。

下面介绍几个SPI的通信实例
上面仅介绍了最基本的交换字节的时序。实际上,SPI要想与从机完成真正的通信,也有更高维度的数据帧结构:指令码+读写数据。每个SPI从机芯片都规定了指令集,指令集中不同的指令码对应不同的功能。下一小节将详细介绍W25Q64的指令集,本节仅看三个演示(默认【模式0】):

  1. 发送指令:向SS指定的设备,发送指令(0x06)。
图11-4 时序图——SPI发送指令

由于上图是软件模拟SPI时序,所以MOSI的数据变化(那个上升沿)没有紧贴SCK下降沿,但是在硬件模拟SPI中是紧贴的。

  1. 指定地址写:向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)。
图11-5 时序图——SPI指定地址写

主机:发指令 0 x 02 + 发 A d d r e s s [ 23 : 16 ] + 发 A d d r e s s [ 15 : 8 ] + 发 A d d r e s s [ 7 : 0 ] + 发 D a t a 主机:发指令0x02+发Address[23:16]+发Address[15:8]+发Address[7:0]+发Data 主机:发指令0x02+Address[23:16]+Address[15:8]+Address[7:0]+Data

  1. 指定地址读:向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)。
图11-6 时序图——SPI指定地址读

主机:发指令 0 x 03 + 发 A d d r e s s [ 23 : 16 ] + 发 A d d r e s s [ 15 : 8 ] + 发 A d d r e s s [ 7 : 0 ] + 收 D a t a 主机:发指令0x03+发Address[23:16]+发Address[15:8]+发Address[7:0]+收Data 主机:发指令0x03+Address[23:16]+Address[15:8]+Address[7:0]+Data

首先可以观察到,由于从机SPI协议由硬件控制,所以从机发送过来的数据,其数据变化边沿都是紧贴着时钟下降沿完成的。并且,如果最后接收完一个字节后时钟仍为低电平,那么从机会继续将下一个地址的数据发送过来,就实现了“连续地址读”。

11.2 W25Q64简介

W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储(电脑BIOS固件)等场景。也就是如果程序需要存储大量的数据,就可以考虑外挂这款芯片。

  • 存储介质:Nor Flash(闪存)。还有Flash一种是Nand Flash。
  • 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)。两重SPI是将MOSI和MISO同时用于收发;四重SPI是再加上写保护WP、数据保持HOLD进行收发数据,共四根线同时收发数据。
  • 存储容量(24位地址):

W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte(本节所使用)
W25Q128:128Mbit / 16MByte
W25Q256:256Mbit / 32MByte

图11-7 W25Q64实物图及原理图
表11-1 W25Q64引脚说明
引脚功能
VCC、GND电源(2.7~3.6V)
CS(SS)SPI片选
CLK(SCK)SPI时钟
DI(MOSI)SPI主机输出从机输入
DO(MISO)SPI主机输入从机输出
WP写保护
HOLD数据保持,用于SPI总线进入中断

上面原理图中可以看出,WP和HOLD两根线都接到了VCC正极,那就表示这两个功能暂时没有用到。

图11-8 W25Q64电路框图
  • 芯片层级结构:8MB存储空间–>128个64KB块–>16个4KB扇区–>16个256B页
  • SPI控制逻辑:芯片内部进行地址锁存、数据读写等操作,都可以由控制逻辑自动完成,外部芯片主要关注与控制逻辑进行数据交互即可
  • 状态寄存器:非常重要,指明芯片是否处于忙状态、是否写使能、是否写保护等。
  • 写控制逻辑:配合WP引脚实现硬件写保护。
  • 高电压生成器:配合Flash进行编程,用于击穿内部晶体管,以实现掉电不丢失数据的特性。
  • 页地址锁存/寄存器:锁存3字节地址的高两个字节。通过写保护和行解码,来选择要操作哪一页。
  • 字节地址锁存/寄存器:锁存3字节地址的最低一个字节。通过列解码和256字节页缓存,来进行指定特定地址的读写操作。由于配有计数器,所以可以很容易实现从指定地址开始,连续读写多个字节。
  • 256字节页缓存区:是一个256字节的RAM存储器,通过这个RAM缓冲区以实现数据读写。写入数据时,为了跟上SPI通信速度,会先将数据放到RAM缓存区里,时序结束后,芯片才会将数据复制到Flash中,所以 单次连续写入数据禁止超过256个字节,并且写时序后芯片会进入忙状态(状态寄存器)。读取数据时由于只需要看Flash电路状态,所以没有限制。

与RAM支持直接读写、覆盖读写不同,为了兼顾掉电不丢失与存储容量大、成本低的特点,Flash会在操作的便捷性上做出一些妥协和让步。于是下面是 Flash操作注意事项

写入操作时:

  • 写入操作前必须先进行写使能,一个写使能只能保证后面一条写操作的执行。这样设置是防止误操作。
  • 每个数据位只能由1改写为0,不能由0改写为1。所以写入数据前必须先擦除(发送擦除指令),擦除后所有数据位变为1。
  • 擦除必须按照 最小擦除单元(扇区) 进行,可以选择的擦除单元有整个芯片、块、扇区。要想不丢失数据,就需要先将所有的数据读取出来,擦除后再统一写入;或者直接为单个字节数据占用一个扇区,就不需要先读取了。
  • 最多连续写入一页的数据(256字节),超过页尾位置的数据,会回到页首覆盖写入。也就是注意地址不要跨越页尾。
  • 写入操作结束后,芯片进入忙状态,不响应新的读写操作。可以使用“读状态寄存器指令”,BUSY为0时芯片空闲。
  • 写入操作总结:写使能–>(备份数据)–>擦除–>等待BUSY位为0–>写入数据–>等待BUSY位为0–>其他操作。

读取操作时:

  • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
图11-9 W25Q64指令集(部分)
  • 更多指令集详细信息可以查看W25Q64芯片手册的“11.2.2 Instruction Set Table 1”。

11.3 实验:软件SPI读写W25Q64

需求:用stm32四个引脚控制高低电平,与W25Q64进行通信。在OLED上显示:

  • 第一行:显示MID(Manufacturer)和DID(Device)。MID是厂商ID(0xEF),DID是设备ID(0x4017)。
  • 第二行:显示写入的四个数据。
  • 第三行:显示读出的四个数据。
图11-10 软件SPI读写W25Q64-接线图

实际上使用软件模拟SPI,是可以任意选择端口的。但是为了后续硬件SPI实验不用再拆线,所以这里选择和stm32上SPI外设引脚。

图11-11 软件SPI读写W25Q64-代码调用(非库函数)

下面是代码展示:
- main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "W25Q64.h"

uint8_t ArrayWrite[4] = {0x11,0x22,0x33,0x44};
uint8_t ArrayRead[4];

int main(void){
    OLED_Init();   //OLED初始化
    W25Q64_Init(); //W25Q64初始化
    
    //初始化OLED显示
    OLED_ShowString(1,1,"MID:FF DID:FFFF ");
    OLED_ShowString(2,1,"W:FF FF FF FF");
    OLED_ShowString(3,1,"R:FF FF FF FF");
    
    //读取W25Q64的ID号
    W25Q64_ID W25Q64_ID_Structure;
    W25Q64_ReadID(&W25Q64_ID_Structure);
    OLED_ShowHexNum(1,5,W25Q64_ID_Structure.MID,2);
    OLED_ShowHexNum(1,12,W25Q64_ID_Structure.DID,4);
    
    //写入数据
    W25Q64_EraseSector(0x00000000);               //写擦除
    W25Q64_PageProgram(0x00000000, ArrayWrite, 4);//写数据
    //读取数据
    W25Q64_ReadByte(0x00000000, ArrayRead, 4);
    
    //将读写数据显示到OLED上
    uint16_t i;
    for(i=0;i<4;i++){
        OLED_ShowHexNum(2,3+3*i,ArrayWrite[i],2);
        OLED_ShowHexNum(3,3+3*i,ArrayRead[i],2);
    }
    while(1){
    };
}

- SPI_User.h

#ifndef __SPI_USER_H
#define __SPI_USER_H

//SPI初始化
void SPI_User_Init(void);
//SPI起始信号
void SPI_User_Start(void);
//SPI终止信号
void SPI_User_Stop(void);
//SPI交换一个字节(模式0)
uint8_t SPI_User_SwapByte(uint8_t SendByte);

#endif

- SPI_User.c

#include "stm32f10x.h"                  // Device header

//SPI-SS引脚写操作
void SPI_User_W_SS(uint8_t BitValue){
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
//SPI-SCK引脚写操作
void SPI_User_W_SCK(uint8_t BitValue){
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
//SPI-MOSI引脚写操作
void SPI_User_W_MOSI(uint8_t BitValue){
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
//SPI-MISO引脚读操作
uint8_t SPI_User_R_MISO(void){
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

//SPI初始化
void SPI_User_Init(void){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    //初始化CLK、SS、MOSI-推挽输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化MISO-上拉输入
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    SPI_User_W_SS(1); //SS默认高电平
    SPI_User_W_SCK(0);//模式0:SCK默认低电平
}
    
//SPI起始信号
void SPI_User_Start(void){
    SPI_User_W_SS(0);
}

//SPI终止信号
void SPI_User_Stop(void){
    SPI_User_W_SS(1);
}

//SPI交换一个字节(模式0)
uint8_t SPI_User_SwapByte(uint8_t SendByte){
    uint8_t i;
    //实现方法一:使用掩码依次提取数据的每一位
    uint8_t RecByte = 0x00;
    for(i=0;i<8;i++){
        SPI_User_W_MOSI((0x80>>i) & SendByte);
        SPI_User_W_SCK(1);//SCK上升沿
        if(SPI_User_R_MISO()==1){
            RecByte = (0x80>>i) | RecByte;
        }
        SPI_User_W_SCK(0);//SCK下降沿
    }
//    //实现方法二:使用循环移位模型
//    for(i=0;i<8;i++){
//        SPI_User_W_MOSI(0x80 & SendByte);
//        SendByte <<= 1;
//        SPI_User_W_SCK(1);//SCK上升沿
//        if(SPI_User_R_MISO()){
//            SendByte |= 0x01;
//        }
//        SPI_User_W_SCK(0);//SCK下降沿
//    }
    return RecByte;
}

- W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H

//W25Q64的ID结构体
typedef struct{
    uint8_t MID;
    uint16_t DID;
}W25Q64_ID;

//W25Q64的指令码
#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_EAD                          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


//W25Q64初始化
void W25Q64_Init(void);
//读取W25Q64的ID
void W25Q64_ReadID(W25Q64_ID* ID_struct);
//页编程
void W25Q64_PageProgram(uint32_t start_addr, uint8_t *wByteArray, uint16_t count);
//写擦除-扇区
void W25Q64_EraseSector(uint32_t erase_addr);
//读取数据-读数据可以跨页
void W25Q64_ReadByte(uint32_t start_addr, uint8_t *rByteArray, uint32_t count);

#endif

- SPI_User.c

#include "stm32f10x.h"                  // Device header
#include "SPI_User.h"
#include "W25Q64.h"

//W25Q64初始化
void W25Q64_Init(void){
    SPI_User_Init();
}

//读取W25Q64的ID
void W25Q64_ReadID(W25Q64_ID* ID_struct){
    SPI_User_Start();
    SPI_User_SwapByte(W25Q64_JEDEC_ID);
    ID_struct->MID = SPI_User_SwapByte(W25Q64_DUMMY_BYTE);
    ID_struct->DID = SPI_User_SwapByte(W25Q64_DUMMY_BYTE);
    ID_struct->DID <<= 8;
    ID_struct->DID |= SPI_User_SwapByte(W25Q64_DUMMY_BYTE);    
    SPI_User_Stop();
}

//发送写使能指令
void W25Q64_WriteEnable(void){
    SPI_User_Start();
    SPI_User_SwapByte(W25Q64_WRITE_ENABLE);
    SPI_User_Stop();
}

//等待W25Q64恢复成空闲状态
void W25Q64_WaitBusy(void){
    SPI_User_Start();
    SPI_User_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//读状态寄存器1
    while((SPI_User_SwapByte(W25Q64_DUMMY_BYTE)&0x01) == 0x01);//Busy位为0就一直等待
    SPI_User_Stop();
}

//页编程
void W25Q64_PageProgram(uint32_t start_addr, uint8_t *wByteArray, uint16_t count){
    uint16_t i;
    W25Q64_WaitBusy();                          //等待忙状态置0
    W25Q64_WriteEnable();                       //写使能
    
    SPI_User_Start();
    SPI_User_SwapByte(W25Q64_PAGE_PROGRAM);     //写指令
    SPI_User_SwapByte(start_addr>>16);          //写地址[23:16]
    SPI_User_SwapByte(start_addr>>8);           //写地址[15:8]
    SPI_User_SwapByte(start_addr);              //写地址[7:0]
    for(i=0;i<count;i++){
        SPI_User_SwapByte(*(wByteArray+i));     //写数据
    }    
    SPI_User_Stop();
}

//写擦除-扇区
void W25Q64_EraseSector(uint32_t erase_addr){
    W25Q64_WaitBusy();                          //等待忙状态置0
    W25Q64_WriteEnable();                       //写使能
    
    SPI_User_Start();
    SPI_User_SwapByte(W25Q64_SECTOR_ERASE_4KB); //擦除指令
    SPI_User_SwapByte(erase_addr>>16);          //擦除地址[23:16]
    SPI_User_SwapByte(erase_addr>>8);           //擦除地址[15:8]
    SPI_User_SwapByte(erase_addr);              //擦除地址[7:0]
    SPI_User_Stop();
}

//读取数据-读数据可以跨页
void W25Q64_ReadByte(uint32_t start_addr, uint8_t *rByteArray, uint32_t count){
    uint32_t i;
    W25Q64_WaitBusy();                       //等待忙状态置0
    SPI_User_Start();
    SPI_User_SwapByte(W25Q64_READ_DATA);     //读指令
    SPI_User_SwapByte(start_addr>>16);       //读地址[23:16]
    SPI_User_SwapByte(start_addr>>8);        //读地址[15:8]
    SPI_User_SwapByte(start_addr);           //读地址[7:0]
    for(i=0;i<count;i++){
        *(rByteArray+i) = SPI_User_SwapByte(W25Q64_DUMMY_BYTE);//读数据
    }
    SPI_User_Stop();
}

编程感想:

  1. 初始化数组的时候,一定要记得加0x,否则默认就是十进制数。
  2. GPIO引脚选中的时候,格式为GPIO_Pin_1,而不是GPIO_PinSource1

11.4 SPI通信外设

STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,不仅SPI性能更高、同时也减轻CPU的负担。下面是stm32中SPI的性能参数(粗体表示使用中的默认配置)

  • 可配置8位/16位数据帧、高位先行/低位先行
  • 时钟频率: fPCLK/(2,4,8,16,32,64,128,256),也就是外设时钟分频得来。APB2中,fPCLK=72MHz;APB1中,fPCLK=36MHz。
  • 支持多主机模型、主机操作、从机操作。
  • 可精简为半双工/单工通信,一般不用。
  • 支持DMA
  • 兼容I2S协议(一种数字音频信号传输的专用协议)。
  • STM32F103C8T6 硬件SPI资源:SPI1(APB2)、SPI2(APB1)。
表11-2 SPI引脚复用表
引脚SPI1SPI2
NSSPA4/PA15PB12
SCKPA5/PA3PB13
MISOPA6/PA4PB14
MOSIPA7/PA5PB15

注:斜杠后引脚定义表示引脚重映射

下面介绍stm32中SPI外设的电路框图:

图11-12 SPI外设电路框图

寄存器配合介绍:

  • 移位寄存器:右侧的数据一位一位地从MOSI输出,MOSI的数据一位一位地移到左侧数据位。
  • LSBFIRST控制位:用于控制移位寄存器是低位先行(1)还是高位先行(0)。
  • MISO和MOSI的交叉:用于切换主从模式。不交叉时为主机模式,交叉时为从机模式。
  • 接收缓冲区、发送缓冲区:实际上分别就是接收数据寄存器RDR、发送数据缓冲区TDR。TDR和RDR占用同一个地址,统一叫作DR。移位寄存器空时,TXE标志位置1,TDR移入数据,下一个数据移入到TDR;移位寄存器接收完毕(同时也标志着移出完成),RXNE标志位置1,数据转运到RDR,此时需要尽快读出RDR,以防止被下一个数据覆盖。

细节:SPI为全双工同步通信,所以为一个移位寄存器、两个缓冲区;IIC为单工通信,所以只需要一个移位寄存器、一个缓冲区;USRT为全双工异步通信,所以需要两个移位寄存器、两个缓冲区,且这两套分别独立。

控制逻辑介绍:

  • 波特率发生器:本质是一个分频器,用于产生SCK时钟。输入时钟就是外设时钟fPCLK=72MHz/36MHz。每产生一个时钟,就移入/移出一个比特。SPI_CR1中的[BR2,BR1,BR0]用于产生分频系数。
  • SPI_CR1:SPI控制寄存器1,下面简单介绍一下。详细可以参考中文数据手册“23.5 SPI和I2S寄存器描述”一节。
  • SPE(SPI Enable):SPI使能,就是SPI_Cmd函数配置的位。
  • BR(Baud Rate):配置波特率,也就是SCK时钟频率。
  • MSTR(Master):配置主机模式(1)、从机(0)模式。
  • CPOL、CPHA:用于选择SPI的四种模式。
图11-13 SPI外设简化框图-SPI主机模式
  • 波特率发生器:用于产生SCK时钟。
  • 数据控制器:根据配置,控制SPI外设电路的运行。
  • 字节交换过程:交换完毕,移位寄存器空,则TXE位置1、RXNE位置1,TDR会自动转运数据到移位寄存器,RDR数据等待用户读取。
  • 开关控制【代码】:SPI外设使能。
  • GPIO【代码】:用于各引脚的初始化。
  • 从机使能引脚SS【代码】:并不存在于SPI硬件外设中,实际使用随便指定一个GPIO口(例如PA4)即可。在一主多从模式下,GPIO模拟SS是最佳选择。

上面介绍了stm32中SPI外设的基本原理,在实际书写代码的过程中,使用一个结构体便可以直接配置 波特率发生器 和 字节交换的默认模式,这是SPI外设内部便会自动工作,用户额外需要关心的只是何时读写DR。下面介绍读写时序的流程,分别是性能更高、使用复杂的“主模式全双工连续传输”,以及性能较低、常用且简单易学的“非连续传输”:

图11-14 主模式全双工连续传输-时序图

本模式可以实现数据的连续传输。

  • 连续写入数据:只要TXE置1,就立马进中断写入数据(会同时清除TXE位);当写入到最后一个数据时,等待BSY位清除,发送流程完毕。
  • 连续读出数据:只要RXNE位置1,就立马进中断读出数据(会同时清除RXNE位)。若不及时读出,现有数据就会被新的数据覆盖。

评价:连续数据流传输对于软件的配合要求较高,需要在每个标志位产生后及时读写数据,整个发送和接收的流程是交错的,但是传输效率是最高的。对传输效率有严格要求才会用到此模式,否则一般采用下面更为简单的“非连续传输”。

图11-15 非连续传输-时序图

本模式对于程序设计非常友好。

  • 字节交换流程:最开始等待TXE位置1,发送一个数据(会自动清除TXE);等待RXNE置1,读取数据。再进行下一次的字节交换。

评价:非连续传输会损失数据传输效率,数据传输速率越快,损失越明显。

a) 软件SPI
b) 硬件SPI
图11-16 软/硬件波形图对比-SPI指定地址写

从上面SPI软/硬件波形的对比来看,硬件SPI的一大特点就是数据变化紧贴SCK边沿,而不是像软件SPI那样,因为代码语句的执行会有一定的延迟(即使SCK边沿变化和输出数据变化的代码是挨在一起的)。

11.5 实验:硬件SPI读写W25Q64

需求:与软件SPI读写W25Q64相同,读出W25Q64的ID,并将ID和读写数据都显示在OLED上。

注:根据引脚定义表,选择SPI1的PA5、PA6、PA7进行通信,另外选择PA4作为片选引脚SS,于是引脚选择也就和软件SPI相同。

接线图代码调用 与上一个实验——“软件SPI读写W25Q64”一致,只是将SPI_User中的引脚变化使用硬件来实现了。

图11-17 硬件读写W25Q64-代码调用(非库函数)

所以下面代码展示main.cW25Q64.hW25Q64.cSPI_User.h与源文件相同,只是SPI_User.c中只是将涉及到SPI引脚变化的操作都替换成库函数了,具体的变化过程参考图11-15“非连续传输”时序图及其说明。

- SPI_User.c

#include "stm32f10x.h"                  // Device header

//SPI-SS引脚写操作
void SPI_User_W_SS(uint8_t BitValue){
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

//SPI初始化
void SPI_User_Init(void){
    //1.开启外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//GPIO时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //SPI1时钟
    //2.初始化端口
    //初始化SS-推挽输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化CLK、MOSI-外设复用推挽输出
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化MISO-上拉输入
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //3.配置SPI
    SPI_InitTypeDef SPI_InitStructure;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; //APB2的2分频-36MHz
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;                       //第一个边沿采样,第二个边沿输出
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;                         //时钟空闲时低电平
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;                  //数据位宽8bit
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //SPI双线全双工
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;                 //高位先行
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;                      //主机模式
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                          //软件自定义片选信号
    SPI_InitStructure.SPI_CRCPolynomial = 0x0007;                      //CRC用不到,所以默认值7
    SPI_Init(SPI1, &SPI_InitStructure);
    //4.SPI使能
    SPI_Cmd(SPI1, ENABLE);
    
    SPI_User_W_SS(1);//默认不选中从机
}
    
//SPI起始信号
void SPI_User_Start(void){
    SPI_User_W_SS(0);
}

//SPI终止信号
void SPI_User_Stop(void){
    SPI_User_W_SS(1);
}

//SPI交换一个字节(模式0)
uint8_t SPI_User_SwapByte(uint8_t SendByte){
    while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)!=SET); //等待TXE置1
    SPI_I2S_SendData(SPI1,SendByte);                          //发送数据到TDR
    while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)!=SET);//等待RNXE置1
    return SPI_I2S_ReceiveData(SPI1);                         //从RDR接收数据
}

编程感想:

  1. Keil小技巧:没有代码自动补全时,就按Ctrl+Alt+Space,但记得先把系统中切换输入的Ctrl+Space快捷键取消。
  2. 关于是否清除标志位。虽然在11-14、11-15的SPI时序图讲解中,手册上写明了TXE和RXNE“由硬件置位并由软件清除”,但是这并不代表需要一条专门的语句来清除标志位,比如SPI中就是读写数据的过程中就自动清除了,所以具体还需要查看数据手册的描述。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/584535.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Acrel-2000系列监控系统在亚运手球比赛馆建设

10kV供配电工程中的应用 摘要:智能化配电监控系统是数字化和信息化时代应运而生的产物&#xff0c;已经被广泛应用于电网用户侧楼宇、体育场馆、科研设施、机场、交通、医院、电力和石化行业等诸多领域的高/低压变配电系统中。安科瑞自研的Acrel-2000系列监控系统可监控高压开…

【自动化测试】自动化测试框架那些事儿!

无论是在自动化测试实践&#xff0c;还是日常交流中&#xff0c;经常听到一个词&#xff1a;框架。在教学的过程中&#xff0c;同学们一直对“框架”这个词知其然不知其所以然。 最近看了很多自动化相关的资料&#xff0c;加上一些实践&#xff0c;算是对“框架”有了一些理解…

OceanMind海睿思入选《2023中国企业数智化转型全景图中国数据智能产业图谱》

近日&#xff0c;国内知名大数据产业创新服务媒体数据猿携手上海大数据联盟发布了《2023中国企业数智化转型升级服务全景图/产业图谱》和《2023中国数据智能产业图谱》。 两份图谱系统梳理了中国数智化转型升级及数据智能行业发展现状和脉络&#xff0c;评选出极具商业合作价值…

​​​​Linux Shell 实现一键部署Oracle21 zip包方式

oracle前言 Oracle开发的关系数据库产品因性能卓越而闻名&#xff0c;Oracle数据库产品为财富排行榜上的前1000家公司所采用&#xff0c;许多大型网站也选用了Oracle系统&#xff0c;是世界最好的数据库产品。此外&#xff0c;Oracle公司还开发其他应用程序和软件。同时&#…

ABeam News|ABeam荣获「SAP BTP 卓越业务分析奖」

近日&#xff0c;「云铸数卯&#xff0c;榫合万象」SAP BTP 客户与生态峰会在桂林圆满落幕。此次年度峰会以全面的视角展现 RISE with SAPSAP BTP如何构建“新型中国企业”数字底座。 ABeam Consulting作为SAP金牌合作伙伴受邀出席&#xff0c;大中华区董事长兼总经理中野洋辅…

Ubuntu Go语言环境配置【GPT-4版】

目录 go语言的培训网课安装goGPT-4给出的过程在VSCode里怎样正确配置好Go的调试相关设置&#xff1f;如果我在Visual Studio Code中安装Go语言的相关工具总是失败怎么办&#xff1f;我已重启Visual Studio Code&#xff0c;接下来应该怎样检查日志&#xff1f;如果我还是不能成…

Vue2中给对象添加新属性界面不刷新

Vue2中给对象添加新属性界面不刷新? Vue2.x的响应式 实现原理 对象类型&#xff1a;通过Object.defineProperty()对属性的读取、修改进行拦截&#xff08;数据劫持&#xff09;。数组类型&#xff1a;通过重写更新数组的一系列方法来实现拦截。&#xff08;对数组的变更方法…

敏捷世界还需要有QA吗?

敏捷开发模型现在已经被越来越多的公司熟知并使用&#xff0c;很多公司会选择敏捷开发模式的转型&#xff0c;其主要目的就是为了不断适应变化以及客户快速交付的诉求。为什么敏捷开发能够被诸多大众接受&#xff1f;可以从两个维度来看&#xff1a; 首先&#xff0c;从开发人员…

条码系统(PC/PDA)对接NC接口功能

条码系统功能清单功能模块功能菜单明细菜单支持终端功能描述业务流程框架pc端框架搭建 PC端整体框架工具搭建,提供字典配置,PDA端的参数配置,NC数据源,NC接口调用工具等 PDA端框架搭建 PDA端安卓端框架基础功能APP创建,数据源,功能菜单等 服务器环境部署 PC端/PDA端pc端/P…

使用YOLOV8实现滑块缺口验证码识别,并使用Fastdeploy快速部署,精度高达99%!!

前言:首先大家对滑块缺口验证码的识别应该有很多经验了,大部分人可能阅读过我的文章或者其他人的文章,能从各方了解到,滑块缺口的各种实现方式,例如模板匹配、边缘检测、提取透明通道、yolov5,paddledection等,此篇文章将会讲解yolov8的使用和训练,yolov8相对于yolov5有…

真心牛x,阿里出品2023最新版Spring全家桶进阶笔记流出,堪称Java程序员跳槽神器

最近小伙伴在我后台留言是这样的&#xff1a; ​现在就这光景&#xff0c;不比以前&#xff0c;会个CRUD就有人要&#xff0c;即使大部分公司依然只需要做CRUD的事情......现在去面试&#xff0c;只会CRUD还要被吐槽&#xff1a; ​面试造火箭&#xff0c;工作拧螺丝&#xff0…

六级备考23天|CET-6|翻译技巧5|2019年12月真题|翻译荷花lotus|11:05-12:05

目录 一、中文 句子1 PRACTICE ANSWER 句子2 PRACTICE ANSWER 句子3 ​ PRACTICE ANSWER 句子4 PRACTICE ANSWER 句子5 PRACTICE ANSWER 句子6 PRACTICE ANSWER ​ 答案整合​ 一、中文 句子1 荷花是中国的名花之一&#xff0c;深受人们喜爱。 PRACTICE Lotus is one…

CMakeLists.txt 文件详解

目录 CMakeLists.txt 常见内容和结构: 文件中的命令和配置&#xff1a; 官方文档&#xff1a; CMakeLists.txt CMakeLists.txt 文件是用于描述 CMake 构建过程和项目配置的文件。它包含了一系列 CMake 命令、变量设置和流程控制结构&#xff0c;用于告诉 CMake 如何生成适合…

pycharm中关于debug模式中按钮的介绍

文章目录 前言一、pycharm中debug的按钮介绍&#xff08;横排按钮&#xff09;二、pycharm中debug的按钮介绍&#xff08;竖排按钮&#xff09;总结 前言 遇到了debug模式下的调试按钮问题。 一、pycharm中debug的按钮介绍&#xff08;横排按钮&#xff09; 步过&#xff1a;s…

七人拼团系统开发模式,助力企业三个月新增超十万会员!

现在市场上最不缺的就是好产品&#xff0c;产品在市场上目前已经趋于饱和。要想让自己的产品或企业快速打入市场&#xff0c;最好的办法是结合好的营销模式&#xff0c;让企业快速起步裂变提升销量。现在就有这么一款模式&#xff0c;助力企业在三个月内就新发展了超十万会员&a…

MySql高可用方案

MySql高可用方案 1、主从、主主(互为主从)keepalived 2、MHA MHA&#xff08;Master High Avaliable&#xff09; 是一款 MySQL 开源高可用程序&#xff0c;MHA 在监测到主实例无响应后&#xff0c;可以自动将同步最靠前的 Slave 提升为 Master&#xff0c;然后将其他所有的 S…

【JavaSE】Java基础语法(三十三):File 一文详解

文章目录 1. File类概述和构造方法2. 绝对路径和相对路径3. File类创建功能【应用】4. File类删除功能【应用】5. File类判断和获取功能【应用】6. 6File类练习一【应用】7. File类练习二【应用】 1. File类概述和构造方法 File类介绍 它是文件和目录路径名的抽象表示文件和目录…

阿里面试测试工程师,水太深,我把握不住了......

前言 去阿里面试测试工程师&#xff0c;这里面水太深&#xff0c;什么未来规划&#xff0c;职业发展的东西都是虚拟的&#xff0c;作者还太年轻&#xff0c;没有那个经历&#xff0c;把握不住。项目只有几个&#xff0c;开心快乐就行&#xff0c;不PK&#xff0c;文明PK。 很多…

组合总和 II

1题目 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用 一次 。 注意&#xff1a;解集不能包含重复的组合。 示例 1: 输入: candidates [10,1…

搭建LightPicture开源免费图床系统「公网远程控制」

文章目录 1.前言2. Lightpicture网站搭建2.1. Lightpicture下载和安装2.2. Lightpicture网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 转载自cpolar极点云的文章&#xff1a;【搭建私人图床】使用LightPicture开源…