SPI 实验

SPI介绍

SPI 是英语 Serial Peripheral interface 缩写,顾名思义就是串行外围设备接口。SPI 通信协 议是 Motorola 公司首先在其 MC68HCXX 系列处理器上定义的。SPI 接口是一种高速的全双工 同步的通信总线,已经广泛应用在众多 MCU、存储芯片、AD 转换器和 LCD 之间。大部分 STM32 是有 3 个 SPI 接口,本实验使用的是 SPI2。

我们先看 SPI 的结构框图,了解它的大致功能。 

SPI 实验_第1张图片 围绕框图,我们展开介绍一下 SPI 的引脚信息、工作原理以及传输方式,把 SPI 的 4 种工作方式放在后面讲解。

SPI 的引脚信息:

MISO(Master In / Slave Out)主设备数据输入,从设备数据输出。

MOSI(Master Out / Slave In)主设备数据输出,从设备数据输入。

SCLK(Serial Clock)时钟信号,由主设备产生。

CS(Chip Select)从设备片选信号,由主设备产生。

SPI 的工作原理:在主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存 器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机,从机 也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中 的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作,主机只需忽略 接收到的字节。反之,若主机要读取从机的一个字节,就必须发送一个空字节引发从机传输。

SPI 的传输方式:SPI 总线具有三种传输方式:全双工、单工以及半双工传输方式。

全双工通信,就是在任何时刻,主机与从机之间都可以同时进行数据的发送和接收。

单工通信,就是在同一时刻,只有一个传输的方向,发送或者是接收。

半双工通信,就是在同一时刻,只能为一个方向传输数据。

SPI 工作模式 

STM32 要与具有 SPI 接口的器件进行通信,就必须遵循 SPI 的通信协议。每一种通信协议 都有各自的读写数据时序,当然 SPI 也不例外。SPI 通信协议就具备 4 种工作模式,在讲这 4 种 工作模式前,首先先知道两个单词 CPOL 和 CPHA。

CPOL,详称 Clock Polarity,就是时钟极性,当主从机没有数据传输的时候即空闲状态, SCL 线的电平状态,假如空闲状态是高电平,CPOL=1;若空闲状态时低电平,那么 CPOL = 0。

CPHA,详称 Clock Phase,就是时钟相位。在这里先科普一下数据传输的常识: 同步通信 时,数据的变化和采样都是在时钟边沿上进行的,每一个时钟周期都会有上升沿和下降沿两个 边沿,那么数据的变化和采样就分别安排在两个不同的边沿,由于数据在产生和到它稳定是需 要一定的时间,那么假如我们在第 1 个边沿信号把数据输出了,从机只能从第 2 个边沿信号去 采样这个数据。

CPHA 实质指的是数据的采样时刻,CPHA= 0 的情况就表示数据的采样是从第 1 个边沿信 号上即奇数边沿,具体是上升沿还是下降沿的问题,是由 CPOL 决定的。这里就存在一个问题: 当开始传输第一个 bit 的时候,第 1 个时钟边沿就采集该数据了,那数据是什么时候输出来的 呢?那么就有两种情况:一是 CS 使能的边沿,二是上一帧数据的最后一个时钟沿。

CPHA=1 的情况就是表示数据采样是从第 2 个边沿即偶数边沿,它的边沿极性要注意一点, 不是和上面 CPHA=0 一样的边沿情况。前面的是奇数边沿采样数据,从 SCL 空闲状态的直接 跳变,空闲状态是高电平,那么它就是下降沿,反之就是上升沿。由于 CPHA=1 是偶数边沿采 样,所以需要根据偶数边沿判断,假如第一个边沿即奇数边沿是下降沿,那么偶数边沿的边沿 极性就是上升沿。不理解的,可以看一下下面 4 种 SPI 工作模式的图。

由于 CPOL 和 CPHA 都有两种不同状态,所以 SPI 分成了 4 种模式。我们在开发的时候, 使用比较多的是模式 0 和模式 3。下面请看表 35.1.1.2.1 SPI 工作模式表。SPI 实验_第2张图片下面分别对 SPI 的 4 种工作模式中的0和1进行分析:SPI 实验_第3张图片SPI 实验_第4张图片SPI 寄存器SPI 实验_第5张图片该寄存器控制着 SPI 很多相关信息,包括主设备模式选择,传输方向,数据格式,时钟极 性、时钟相位和使能等。下面讲解一下本实验配置的位,位 CPHA 置 1,数据采样从第二个时 钟边沿开始;位 CPOL 置 1,在空闲状态时,SCK 保持高电平;位 MSTR 置 1,配置为主机模 式;位 BR[2:0]置 7,使用 256 分频,速度最低;位 SPE 置 1,开启 SPI 设备;位 LSBFIRST 置 0,MSB 先传输;位 SSI 置 1,禁止软件从设备,即做主机;位 SSM 置 1,软件片选 NSS 控制; 位 RXONLY 置 0,传输方式采用的是全双工模式;位 DFF 置 0,使用 8 位数据帧格式。SPI 实验_第6张图片SPI 实验_第7张图片

 

NOR FLASH 简介 

FLASH 是常见的用于存储数据的半导体器件,它具有容量大、可重复擦写、按“扇区/块” 擦除、掉电后数据可继续保存的特性。常见的 FLASH 主要有 NOR FLASH 和 NAND FLASH 两 种类型,它们的特性如表 35.1.2.1.1 所示。NOR 和 NAND 是两种数字门电路,可以简单地认为 FLASH 内部存储单元使用哪种门作存储单元就是哪类型的 FLASH。U 盘,SSD,eMMC 等为 NAND 型,而 NOR FLASH 则根据设计需要灵活应用于各类 PCB 上,如 BIOS,手机等。SPI 实验_第8张图片SPI 实验_第9张图片NOR 与 NAND 在数据写入前都需要有擦除操作,但实际上 NOR FLASH 的一个 bit 可以从 1 变成 0,而要从 0 变 1 就要擦除后再写入,NAND FLASH 这两种情况都需要擦除。擦除操作 的最小单位为“扇区/块”,这意味着有时候即使只写一字节的数据,则这个“扇区/块”上之前 的数据都可能会被擦除。

NOR 的地址线和数据线分开,它可以按“字节”读写数据,符合 CPU 的指令译码执行要 求,所以假如 NOR 上存储了代码指令,CPU 给 NOR 一个地址,NOR 就能向 CPU 返回一个数 据让 CPU 执行,中间不需要额外的处理操作,这体现于表 35.1.2.1.1 中的支持 XIP 特性(eXecute In Place)。因此可以用 NOR FLASH 直接作为嵌入式 MCU 的程序存储空间。

NAND 的数据和地址线共用,只能按“块”来读写数据,假如 NAND 上存储了代码指令, CPU 给 NAND 地址后,它无法直接返回该地址的数据,所以不符合指令译码要求。

若代码存储在 NAND 上,可以把它先加载到 RAM 存储器上,再由 CPU 执行。所以在功 能上可以认为 NOR 是一种断电后数据不丢失的 RAM,但它的擦除单位与 RAM 有区别,且读 写速度比 RAM 要慢得多。

FLASH 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题:一是 FLASH 的使 用寿命,另一个是可能的位反转。

使用寿命体现在:读写上是 FLASH 的擦除次数都是有限的(NOR FLASH 普遍是 10 万次左 右),当它的使用接近寿命的时候,可能会出现写操作失败。由于 NAND 通常是整块擦写,块内 有一位失效整个块就会失效,这被称为坏块。使用 NAND FLASH 最好通过算法扫描介质找出 坏块并标记为不可用,因为坏块上的数据是不准确的。

位反转是数据位写入时为 1,但经过一定时间的环境变化后可能实际变为 0 的情况,反之 亦然。位反转的原因很多,可能是器件特性也可能与环境、干扰有关,由于位反转的的问题可 能存在,所以 FLASH 存储器需要“探测/错误更正(EDC/ECC)”算法来确保数据的正确性。

FLASH 芯片有很多种芯片型号,在我们的 norflash.h 头文件中有定义芯片 ID 的宏定义,对 应的就是不同型号的 NOR FLASH 芯片,比如有:W25Q128、BY25Q128、NM25Q128,它们是 来自不同的厂商的同种规格的 NOR FLASH 芯片,内存空间都是 128M 字,即 16M 字节。它们 的很多参数、操作都是一样的,所以我们的实验都是兼容它们的。

NM25Q128简介

NM25Q128 是一款大容量 SPI FLASH 产品,其容量为 16M。它将 16M 字节的容量分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16 个扇区(Sector),每一个扇区 16 页, 每页 256 个字节,即每个扇区 4K 个字节。NM25Q128 的最小擦除单位为一个扇区,也就是每 次必须擦除 4K 个字节。这样我们需要给 NM25Q128 开辟一个至少 4K 的缓存区,这样对 SRAM 要求比较高,要求芯片必须有 4K 以上 SRAM 才能很好的操作。

NM25Q128 的擦写周期多达 10W 次,具有 20 年的数据保存期限,支持电压为 2.7~3.6V, NM25Q128 支持标准的 SPI,还支持双输出/四输出的 SPI,最大 SPI 时钟可以到 104Mhz(双输 出时相当于 208Mhz,四输出时相当于 416Mhz)。

芯片引脚连接如下:CS 即片选信号输入,低电平有效;DO 是 MISO 引脚,在 CLK 管脚 的下降沿输出数据;WP 是写保护管脚,高电平可读可写,低电平仅仅可读;DI 是 MOSI 引脚, 主机发送的数据、地址和命令从 SI 引脚输入到芯片内部,在 CLK 管脚的上升沿捕获捕获数据; CLK 是串行时钟引脚,为输入输出提供时钟脉冲;HOLD 是保持管脚,低电平有效。

STM32F103 通过 SPI 总线连接到 NM25Q128 对应的引脚即可启动数据传输。

SPI 实验_第10张图片SPI 实验_第11张图片SPI 实验_第12张图片

SPI 实验_第13张图片SPI 实验_第14张图片

代码 

#include "./BSP/SPI/spi.h"

SPI_HandleTypeDef g_spi2_handle;

/* SPI初始化代码 */
void spi2_init(void)
{
    g_spi2_handle.Instance = SPI2;/* SPI2 */
    g_spi2_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;/* 波特率预分频值为256 */
    g_spi2_handle.Init.CLKPhase = SPI_PHASE_2EDGE;/* 串行同步时钟的第二个跳变沿数据被采样 */
    g_spi2_handle.Init.CLKPolarity = SPI_POLARITY_HIGH;/* 串行同步时钟的空闲状态为高电平 */
    g_spi2_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;/* 关闭硬件CRC校验 */
    g_spi2_handle.Init.CRCPolynomial = 7;/* CRC值计算的多项式 */
    g_spi2_handle.Init.DataSize = SPI_DATASIZE_8BIT;/* SPI发送接收8位帧结构 */
    g_spi2_handle.Init.Direction = SPI_DIRECTION_2LINES;/* SPI设置为双线模式 */
    g_spi2_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;/* 数据传输从MSB位开始 */
    g_spi2_handle.Init.Mode  = SPI_MODE_MASTER;/* 设置SPI工作模式,设置为主模式 */
    g_spi2_handle.Init.NSS = SPI_NSS_SOFT;/* NSS信号软件管理:内部NSS信号有SSI位控制 */
    g_spi2_handle.Init.TIMode = SPI_TIMODE_DISABLE;/* 关闭TI模式 */
    HAL_SPI_Init(&g_spi2_handle);/* 初始化 */
    
    __HAL_SPI_ENABLE(&g_spi2_handle);/* 使能SPI2 */
}

/* SPI底层驱动,时钟使能,引脚配置 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    __HAL_RCC_SPI2_CLK_ENABLE();/* SPI2时钟使能 */
    
    GPIO_InitTypeDef gpio_init_struct = {0};
    
    if(hspi->Instance == SPI2)
    {
        __HAL_RCC_GPIOB_CLK_ENABLE();/* SPI2_SCK,SPI2_MOSI,SPI2_MISO脚时钟使能 */
        
        /* SCK引脚模式设置(复用输出) */
        gpio_init_struct.Pin = GPIO_PIN_13;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;
        gpio_init_struct.Pull = GPIO_PULLUP;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOB, &gpio_init_struct);
        
        /* MISO引脚模式设置(复用输出) */
        gpio_init_struct.Pin = GPIO_PIN_14;
        HAL_GPIO_Init(GPIOB, &gpio_init_struct);
        
        /* MOSI引脚模式设置(复用输出) */
        gpio_init_struct.Pin = GPIO_PIN_15;
        HAL_GPIO_Init(GPIOB, &gpio_init_struct);
    }
}

/* SPI2读写一个字节数据 */
uint8_t spi2_read_write_byte(uint8_t txdata)
{
    uint8_t rxdata = 0;
    HAL_SPI_TransmitReceive(&g_spi2_handle, &txdata, &rxdata, 1,1000);
    return rxdata;/* 返回收到的数据 */
}
#include "./BSP/NORFLASH/norflash.h"
#include "./BSP/SPI/spi.h"

/* 初始化SPI NOR FLASH函数 */
void norflash_init(void)
{
    GPIO_InitTypeDef gpio_init_struct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();/* NORFLASH CS脚 时钟使能 */
    
    gpio_init_struct.Pin = GPIO_PIN_12;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;/* 片选引脚输出要为推挽输出,不能是复用推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &gpio_init_struct);/* CS引脚模式设置(复用输出) */
    
    spi2_init();
    
    spi2_read_write_byte(0xff);/* 启动传输, 实际上就是产生8个时钟脉冲, 达到清空DR的作用, 非必需 */
    
    NORFLASH_CS(1);
}

/* 读取NORFLASH数据函数 */
uint8_t norflash_read_data(uint32_t addr)
{
    uint8_t rxdata = 0;
    
    /* 1.拉低片选信号 */
    NORFLASH_CS(0);
    
    /* 2.发送读命令 */
    spi2_read_write_byte(0x03);
    
    /* 3.发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    
    /* 4.读取数据 */
    rxdata = spi2_read_write_byte(0xff);
    
    /* 5.拉高片选信号 */
    NORFLASH_CS(1);
    
    return rxdata;
}

/* 等待空闲函数 */
uint8_t norflash_wait_busy(void)
{
    uint8_t rxdata = 0;
    
    /* 1.拉低片选信号 */
    NORFLASH_CS(0);
    
    /* 2.发送读状态寄存器1指令 */
    spi2_read_write_byte(0x05);
    
    /* 3.读取状态数据 */
    rxdata = spi2_read_write_byte(0xff);
    
    /* 5.拉高片选信号 */
    NORFLASH_CS(1);
    
    return rxdata;
}

/* 扇区擦除函数 */
void norflash_erase_sector(uint32_t addr)
{
    /* 1.写使能 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x06);
    NORFLASH_CS(1);
    
    /* 2.等待空闲 */
    while(norflash_wait_busy() & 0x01);
    
    /* 3.发送扇区擦除指令 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x20);
    
    /* 4.发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    NORFLASH_CS(1);
    
    /* 5.等待空闲 */
    while(norflash_wait_busy() & 0x01);
}

/* 页写函数 */
void norflash_write_page(uint8_t data, uint32_t addr)
{
    /* 1.擦除扇区 */
    norflash_erase_sector(addr);
    
    /* 2.写使能 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x06);
    NORFLASH_CS(1);
    
    /* 3.发送页写指令 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x02);
    
    /* 4.发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    
    /* 5.要写入的数据 */
    spi2_read_write_byte(data);
    NORFLASH_CS(1);
    
    /* 6.等待空闲(等待写入完成) */
    while(norflash_wait_busy() & 0x01);
}
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/NORFLASH/norflash.h"

int main(void)
{
    uint8_t t = 0;
    uint8_t key = 0;
    uint8_t rxdata = 0;
    
    HAL_Init();                         /* 初始化 HAL 库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 传口初始化 */
    
    led_init();                         /* LED初始化 */
    key_init();                         /* KEY初始化 */
    norflash_init();                    /* NORFLASH初始化 */
    
    while(1)
    {
        key = key_scan(0);
        
        if (key == KEY0_PRES) /* KEY0按下,写入数据并显示写入完成 */
        {
//            norflash_write_page('B', 0x123457);/* 在0x123457写‘B’后,0x123456处的‘A’会被擦除 */
            norflash_write_page('A', 0x123456);/* 地址范围0~0xffffff */
            printf("Write Data Finish!\r\n\r\n");/* 显示写入完成 */
        }
        
        if(key == KEY1_PRES) /* KEY1按下,读出数据并显示 */
        {
//            norflash_erase_sector(0x123455);/* 验证扇区擦除的大小为4096byte */
            rxdata = norflash_read_data(0x123456);
            printf("Read Data is : %c\r\n\r\n",rxdata);/* 显示读到的数据 */
        }
        
        t++;
        if (t == 20)
        {
            LED1_TOGGLE(); /* LED1闪烁 */
            t = 0;
        }
        delay_ms(10);
    }
}

你可能感兴趣的:(stm32)