STM32 HAL库SPI读写W25Q128(软件模拟+硬件spi)

1. 引言

在嵌入式系统开发中,SPI(Serial Peripheral Interface)总线是一种常用的串行通信协议,用于在微控制器和外部设备之间进行高速数据传输。W25Q128 是一款常见的 SPI Flash 芯片,具有 128Mbit(16MB)的存储容量,广泛应用于数据存储和程序代码存储等场景。STM32F407 是一款高性能的 ARM Cortex - M4 内核微控制器,它支持硬件 SPI 接口,同时也可以通过软件模拟 SPI 通信。本文将详细介绍基于 STM32F407 HAL 库实现软件模拟 SPI 和硬件 SPI 读写 W25Q128 的方法。

2. 硬件连接

2.1 STM32F407 与 W25Q128 的硬件连接
STM32F407 引脚W25Q128 引脚功能说明
PA5(SCK)CLKSPI 时钟信号
PA6(MISO)DO主设备输入从设备输出
PA7(MOSI)DI主设备输出从设备输入
PA4(NSS)CS片选信号

3. 软件模拟 SPI 读写 W25Q128

3.1 初始化 GPIO 引脚

在软件模拟 SPI 通信中,需要将相应的 GPIO 引脚配置为输出或输入模式。以下是初始化 GPIO 引脚的代码示例:

#include "stm32f4xx_hal.h"

// 定义SPI通信所需的GPIO引脚
// 时钟信号引脚
#define SPI_SCK_PIN GPIO_PIN_5
// 主设备输入从设备输出引脚
#define SPI_MISO_PIN GPIO_PIN_6
// 主设备输出从设备输入引脚
#define SPI_MOSI_PIN GPIO_PIN_7
// 片选信号引脚
#define SPI_NSS_PIN GPIO_PIN_4
// 这些引脚所在的GPIO端口
#define SPI_GPIO_PORT GPIOA

// 初始化SPI通信所需的GPIO引脚
void SPI_GPIO_Init(void) {
    // 定义一个GPIO初始化结构体变量,用于配置GPIO引脚
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 使能GPIOA端口的时钟,因为SPI通信使用的引脚位于GPIOA端口
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 配置时钟信号、主设备输出从设备输入和片选信号引脚
    // 将这些引脚的配置信息存储在GPIO_InitStruct结构体中
    GPIO_InitStruct.Pin = SPI_SCK_PIN | SPI_MOSI_PIN | SPI_NSS_PIN;
    // 设置这些引脚为推挽输出模式
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    // 不使用上拉或下拉电阻
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    // 设置引脚的输出速度为低频
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    // 根据上述配置信息初始化SPI_GPIO_PORT(即GPIOA)端口的相应引脚
    HAL_GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);

    // 配置主设备输入从设备输出引脚
    // 重新设置引脚为SPI_MISO_PIN
    GPIO_InitStruct.Pin = SPI_MISO_PIN;
    // 设置该引脚为输入模式
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    // 不使用上拉或下拉电阻
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    // 根据上述配置信息初始化SPI_GPIO_PORT(即GPIOA)端口的SPI_MISO_PIN引脚
    HAL_GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);

    // 初始时将片选信号引脚拉高,禁用从设备
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);
}
3.2 软件模拟 SPI 位操作

实现 SPI 的位操作函数,包括时钟信号的产生和数据的发送与接收。

/**
 * @brief 发送一个SPI位数据
 * 
 * 此函数用于通过软件模拟SPI协议发送一个位的数据。
 * 它首先将待发送的位数据写入MOSI引脚,然后通过操作SCK引脚产生一个时钟脉冲。
 * 
 * @param bit 待发送的位数据,取值为0或1
 */
void SPI_SendBit(uint8_t bit) {
    // 将待发送的位数据写入MOSI引脚,将bit强制转换为GPIO_PinState类型
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, (GPIO_PinState)bit);
    // 将SCK引脚置高,产生时钟信号的上升沿
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
    // 将SCK引脚置低,产生时钟信号的下降沿,完成一个时钟周期
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
}

/**
 * @brief 接收一个SPI位数据
 * 
 * 此函数用于通过软件模拟SPI协议接收一个位的数据。
 * 它通过操作SCK引脚产生一个时钟脉冲,在时钟上升沿时读取MISO引脚的数据。
 * 
 * @return uint8_t 接收到的位数据,取值为0或1
 */
uint8_t SPI_ReceiveBit(void) {
    // 定义一个变量用于存储接收到的位数据
    uint8_t bit;
    // 将SCK引脚置高,产生时钟信号的上升沿
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
    // 读取MISO引脚的电平状态,将其赋值给bit变量
    bit = HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN);
    // 将SCK引脚置低,产生时钟信号的下降沿,完成一个时钟周期
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
    // 返回接收到的位数据
    return bit;
}
3.3 软件模拟 SPI 字节操作

基于位操作函数,实现字节的发送和接收。

/**
 * @brief 通过软件模拟SPI协议发送一个字节的数据,并同时接收一个字节的数据
 *
 * 该函数会循环8次,每次发送一位数据并接收一位数据,最终组合成一个完整的字节。
 * 发送数据时,从最高位开始逐位发送;接收数据时,同样从最高位开始逐位接收并组合。
 *
 * @param byte 要发送的字节数据
 * @return uint8_t 接收到的字节数据
 */
uint8_t SPI_SendByte(uint8_t byte) {
    // 定义循环变量i用于循环发送和接收每一位数据
    // 定义变量received_byte用于存储接收到的字节数据,初始化为0
    uint8_t i, received_byte = 0;
    // 循环8次,因为一个字节有8位
    for (i = 0; i < 8; i++) {
        // 发送当前位的数据
        // (byte >> (7 - i))将byte右移(7 - i)位,使得要发送的位移动到最低位
        // & 0x01将该位与1进行按位与操作,提取出该位的值
        // 调用SPI_SendBit函数发送该位
        SPI_SendBit((byte >> (7 - i)) & 0x01);
        // 接收一位数据并组合到received_byte中
        // 调用SPI_ReceiveBit函数接收一位数据
        // 将接收到的位左移(7 - i)位,移动到合适的位置
        // 然后与received_byte进行按位或操作,将该位组合到received_byte中
        received_byte |= (SPI_ReceiveBit() << (7 - i));
    }
    // 返回接收到的完整字节数据
    return received_byte;
}
3.4 读写 W25Q128 函数

实现对 W25Q128 的读写操作,包括读取设备 ID、写使能、擦除扇区和写入数据等功能。

/**
 * @brief 读取W25Q128的设备ID
 * 
 * 该函数通过SPI接口向W25Q128发送读取设备ID的命令,然后接收并返回设备ID。
 * 
 * @return uint16_t 读取到的W25Q128设备ID
 */
uint16_t W25Q128_ReadID(void) {
    // 用于存储读取到的设备ID
    uint16_t device_id;
    // 拉低片选信号,选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);
    // 发送读取设备ID的命令码0x90
    SPI_SendByte(0x90);
    // 发送地址字节,这里地址为0x000000
    SPI_SendByte(0x00);
    SPI_SendByte(0x00);
    SPI_SendByte(0x00);
    // 先接收高8位数据,并左移8位存储到device_id的高8位
    device_id = (uint16_t)SPI_SendByte(0xFF) << 8;
    // 再接收低8位数据,并与device_id进行按位或操作,组合成完整的16位设备ID
    device_id |= SPI_SendByte(0xFF);
    // 拉高片选信号,取消选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);
    // 返回读取到的设备ID
    return device_id;
}

/**
 * @brief 使能W25Q128的写操作
 * 
 * 该函数通过SPI接口向W25Q128发送写使能命令,允许后续的写操作。
 */
void W25Q128_WriteEnable(void) {
    // 拉低片选信号,选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);
    // 发送写使能命令码0x06
    SPI_SendByte(0x06);
    // 拉高片选信号,取消选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);
}

/**
 * @brief 擦除W25Q128的指定扇区
 * 
 * 该函数先使能写操作,然后通过SPI接口向W25Q128发送扇区擦除命令和扇区地址,完成扇区擦除操作。
 * 
 * @param sector_address 要擦除的扇区地址
 */
void W25Q128_SectorErase(uint32_t sector_address) {
    // 使能写操作,允许后续的擦除操作
    W25Q128_WriteEnable();
    // 拉低片选信号,选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);
    // 发送扇区擦除命令码0x20
    SPI_SendByte(0x20);
    // 发送扇区地址的高8位
    SPI_SendByte((sector_address >> 16) & 0xFF);
    // 发送扇区地址的中间8位
    SPI_SendByte((sector_address >> 8) & 0xFF);
    // 发送扇区地址的低8位
    SPI_SendByte(sector_address & 0xFF);
    // 拉高片选信号,取消选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);
}

/**
 * @brief 向W25Q128的指定地址写入数据
 * 
 * 该函数先使能写操作,然后通过SPI接口向W25Q128发送页编程命令和写入地址,最后将数据逐字节写入。
 * 
 * @param address 写入数据的起始地址
 * @param data 要写入的数据数组
 * @param length 要写入的数据长度
 */
void W25Q128_PageProgram(uint32_t address, uint8_t *data, uint16_t length) {
    // 使能写操作,允许后续的写入操作
    W25Q128_WriteEnable();
    // 拉低片选信号,选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);
    // 发送页编程命令码0x02
    SPI_SendByte(0x02);
    // 发送写入地址的高8位
    SPI_SendByte((address >> 16) & 0xFF);
    // 发送写入地址的中间8位
    SPI_SendByte((address >> 8) & 0xFF);
    // 发送写入地址的低8位
    SPI_SendByte(address & 0xFF);
    // 循环将数据逐字节写入W25Q128
    for (uint16_t i = 0; i < length; i++) {
        SPI_SendByte(data[i]);
    }
    // 拉高片选信号,取消选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);
}

/**
 * @brief 从W25Q128的指定地址读取数据
 * 
 * 该函数通过SPI接口向W25Q128发送读取数据命令和读取地址,然后将数据逐字节读取到指定数组中。
 * 
 * @param address 读取数据的起始地址
 * @param data 用于存储读取数据的数组
 * @param length 要读取的数据长度
 */
void W25Q128_ReadData(uint32_t address, uint8_t *data, uint16_t length) {
    // 拉低片选信号,选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);
    // 发送读取数据命令码0x03
    SPI_SendByte(0x03);
    // 发送读取地址的高8位
    SPI_SendByte((address >> 16) & 0xFF);
    // 发送读取地址的中间8位
    SPI_SendByte((address >> 8) & 0xFF);
    // 发送读取地址的低8位
    SPI_SendByte(address & 0xFF);
    // 循环将数据逐字节从W25Q128读取到data数组中
    for (uint16_t i = 0; i < length; i++) {
        data[i] = SPI_SendByte(0xFF);
    }
    // 拉高片选信号,取消选中W25Q128
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);
}

4. 硬件 SPI 读写 W25Q128

4.1 初始化硬件 SPI

使用 STM32F407 的硬件 SPI 接口进行通信,需要初始化 SPI 外设。

// 定义一个SPI句柄结构体变量,用于配置和操作SPI1外设
SPI_HandleTypeDef hspi1;

/**
 * @brief 初始化SPI1外设
 * 
 * 此函数用于对SPI1外设进行配置,设置其工作模式、数据方向、数据大小等参数,
 * 并调用HAL库的初始化函数进行初始化。若初始化失败,调用错误处理函数。
 */
void SPI1_Init(void) {
    // 指定使用的SPI外设实例为SPI1
    hspi1.Instance = SPI1;
    // 设置SPI工作模式为主模式,即STM32作为主设备控制通信
    hspi1.Init.Mode = SPI_MODE_MASTER;
    // 设置SPI数据传输方向为双线模式,即同时支持发送和接收数据
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    // 设置SPI数据传输大小为8位,即每次传输一个字节的数据
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    // 设置SPI时钟极性为低电平,即空闲状态下时钟信号为低电平
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
    // 设置SPI时钟相位为第一个边沿采样数据,即数据在时钟信号的第一个边沿被采样
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
    // 设置SPI片选信号为软件控制,即通过软件来控制片选引脚的电平
    hspi1.Init.NSS = SPI_NSS_SOFT;
    // 设置SPI波特率预分频系数为256,用于降低SPI通信的时钟频率
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
    // 设置SPI数据传输的位顺序为高位在前,即先传输数据的最高位
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    // 禁用SPI的TI模式,TI模式通常用于特定的通信协议
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    // 禁用SPI的CRC校验功能,CRC校验用于数据传输的错误检测
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    // 设置CRC多项式的值为7,由于CRC校验已禁用,此值无实际作用
    hspi1.Init.CRCPolynomial = 7;
    // 调用HAL库的SPI初始化函数进行SPI1外设的初始化
    if (HAL_SPI_Init(&hspi1) != HAL_OK) {
        // 若初始化失败,调用错误处理函数进行处理
        Error_Handler();
    }
}

/**
 * @brief SPI外设的底层硬件初始化函数
 * 
 * 此函数用于对SPI外设所使用的GPIO引脚进行初始化配置,
 * 包括使能相关时钟、设置引脚模式、速度和复用功能等。
 * 
 * @param spiHandle 指向SPI句柄结构体的指针,用于判断是哪个SPI外设
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) {
    // 定义一个GPIO初始化结构体变量,用于配置GPIO引脚
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    // 判断要初始化的SPI外设实例是否为SPI1
    if(spiHandle->Instance==SPI1) {
        // 使能SPI1外设的时钟,以便可以对其进行配置和使用
        __HAL_RCC_SPI1_CLK_ENABLE();
        // 使能GPIOA端口的时钟,因为SPI1使用的引脚位于GPIOA端口
        __HAL_RCC_GPIOA_CLK_ENABLE();

        // 配置SPI1的SCK(时钟)、MISO(主设备输入从设备输出)、MOSI(主设备输出从设备输入)引脚
        // 将这些引脚的配置信息存储在GPIO_InitStruct结构体中
        GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
        // 设置这些引脚为复用推挽输出模式,用于SPI通信
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        // 不使用上拉或下拉电阻
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        // 设置引脚的输出速度为非常高,以适应高速SPI通信
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
        // 设置这些引脚的复用功能为SPI1,即作为SPI1的相关信号引脚
        GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
        // 根据上述配置信息初始化GPIOA端口的相应引脚
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        // 配置SPI1的NSS(片选)引脚
        // 重新设置引脚为GPIO_PIN_4
        GPIO_InitStruct.Pin = GPIO_PIN_4;
        // 设置该引脚为推挽输出模式,用于软件控制片选信号
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
        // 不使用上拉或下拉电阻
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        // 设置引脚的输出速度为低频
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
        // 根据上述配置信息初始化GPIOA端口的SPI_NSS_PIN引脚
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
        // 初始时将片选信号引脚拉高,禁用从设备
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    }
}
4.2 读写 W25Q128 函数

使用硬件 SPI 实现对 W25Q128 的读写操作。

/**
 * @brief 使用硬件SPI读取W25Q128的设备ID
 * 
 * 该函数通过硬件SPI接口向W25Q128发送读取设备ID的命令,然后接收并返回设备ID。
 * 
 * @return uint16_t 读取到的W25Q128设备ID
 */
uint16_t W25Q128_ReadID_HardwareSPI(void) {
    // 定义发送数据的数组,包含读取设备ID的命令和地址信息
    uint8_t tx_data[4] = {0x90, 0x00, 0x00, 0x00};
    // 定义接收数据的数组,用于存储读取到的设备ID
    uint8_t rx_data[2];
    // 拉低片选信号,选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    // 通过硬件SPI发送tx_data数组中的4个字节数据
    HAL_SPI_Transmit(&hspi1, tx_data, 4, HAL_MAX_DELAY);
    // 通过硬件SPI接收2个字节的数据到rx_data数组中
    HAL_SPI_Receive(&hspi1, rx_data, 2, HAL_MAX_DELAY);
    // 拉高片选信号,取消选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
    // 将接收到的两个字节数据组合成一个16位的设备ID
    return (uint16_t)rx_data[0] << 8 | rx_data[1];
}

/**
 * @brief 使用硬件SPI使能W25Q128的写操作
 * 
 * 该函数通过硬件SPI接口向W25Q128发送写使能命令,以允许后续的写操作。
 */
void W25Q128_WriteEnable_HardwareSPI(void) {
    // 定义要发送的写使能命令
    uint8_t tx_data = 0x06;
    // 拉低片选信号,选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    // 通过硬件SPI发送写使能命令
    HAL_SPI_Transmit(&hspi1, &tx_data, 1, HAL_MAX_DELAY);
    // 拉高片选信号,取消选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}

/**
 * @brief 使用硬件SPI擦除W25Q128的指定扇区
 * 
 * 该函数先使能写操作,然后通过硬件SPI接口向W25Q128发送扇区擦除命令和扇区地址。
 * 
 * @param sector_address 要擦除的扇区地址
 */
void W25Q128_SectorErase_HardwareSPI(uint32_t sector_address) {
    // 调用写使能函数,允许后续的擦除操作
    W25Q128_WriteEnable_HardwareSPI();
    // 定义发送数据的数组,包含扇区擦除命令和扇区地址信息
    uint8_t tx_data[4] = {0x20, (uint8_t)(sector_address >> 16), (uint8_t)(sector_address >> 8), (uint8_t)sector_address};
    // 拉低片选信号,选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    // 通过硬件SPI发送tx_data数组中的4个字节数据
    HAL_SPI_Transmit(&hspi1, tx_data, 4, HAL_MAX_DELAY);
    // 拉高片选信号,取消选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}

/**
 * @brief 使用硬件SPI向W25Q128的指定地址写入数据
 * 
 * 该函数先使能写操作,然后通过硬件SPI接口向W25Q128发送页编程命令、写入地址和数据。
 * 
 * @param address 写入数据的起始地址
 * @param data 要写入的数据数组指针
 * @param length 要写入的数据长度
 */
void W25Q128_PageProgram_HardwareSPI(uint32_t address, uint8_t *data, uint16_t length) {
    // 调用写使能函数,允许后续的写入操作
    W25Q128_WriteEnable_HardwareSPI();
    // 定义发送数据的数组,包含页编程命令和写入地址信息
    uint8_t tx_data[4] = {0x02, (uint8_t)(address >> 16), (uint8_t)(address >> 8), (uint8_t)address};
    // 拉低片选信号,选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    // 通过硬件SPI发送tx_data数组中的4个字节数据
    HAL_SPI_Transmit(&hspi1, tx_data, 4, HAL_MAX_DELAY);
    // 通过硬件SPI发送要写入的数据
    HAL_SPI_Transmit(&hspi1, data, length, HAL_MAX_DELAY);
    // 拉高片选信号,取消选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}

/**
 * @brief 使用硬件SPI从W25Q128的指定地址读取数据
 * 
 * 该函数通过硬件SPI接口向W25Q128发送读取数据命令和读取地址,然后接收数据。
 * 
 * @param address 读取数据的起始地址
 * @param data 用于存储读取数据的数组指针
 * @param length 要读取的数据长度
 */
void W25Q128_ReadData_HardwareSPI(uint32_t address, uint8_t *data, uint16_t length) {
    // 定义发送数据的数组,包含读取数据命令和读取地址信息
    uint8_t tx_data[4] = {0x03, (uint8_t)(address >> 16), (uint8_t)(address >> 8), (uint8_t)address};
    // 拉低片选信号,选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
    // 通过硬件SPI发送tx_data数组中的4个字节数据
    HAL_SPI_Transmit(&hspi1, tx_data, 4, HAL_MAX_DELAY);
    // 通过硬件SPI接收指定长度的数据到data数组中
    HAL_SPI_Receive(&hspi1, data, length, HAL_MAX_DELAY);
    // 拉高片选信号,取消选中W25Q128芯片
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}

5. 主函数测试

在主函数中调用上述函数进行测试。

/**
 * @brief 主函数,程序的入口点
 * 
 * 此函数完成系统初始化,包括HAL库、系统时钟、SPI相关的GPIO和SPI外设,
 * 然后通过软件模拟SPI和硬件SPI两种方式读取W25Q128的设备ID,
 * 接着分别使用软件模拟SPI和硬件SPI对W25Q128进行扇区擦除、数据写入和读取操作,
 * 最后进入一个无限循环,保持程序持续运行。
 */
int main(void) {
    // 初始化HAL库,这是STM32 HAL库的基础初始化步骤,
    // 它会进行一些底层的硬件初始化和配置,为后续使用HAL库函数做准备
    HAL_Init();

    // 配置系统时钟,设置合适的时钟频率,确保系统各个模块能正常工作
    // 该函数可能包含了对晶振、PLL等时钟源和时钟分频器的配置
    SystemClock_Config();

    // 初始化SPI通信所需的GPIO引脚,包括SCK、MISO、MOSI和NSS引脚,
    // 为软件模拟SPI通信做准备
    SPI_GPIO_Init();

    // 初始化SPI1外设,配置SPI1的工作模式、数据方向、时钟极性等参数,
    // 为硬件SPI通信做准备
    SPI1_Init();

    // 使用软件模拟SPI的方式读取W25Q128的设备ID,
    // 将读取到的设备ID存储在device_id_soft变量中
    uint16_t device_id_soft = W25Q128_ReadID();

    // 使用硬件SPI的方式读取W25Q128的设备ID,
    // 将读取到的设备ID存储在device_id_hard变量中
    uint16_t device_id_hard = W25Q128_ReadID_HardwareSPI();

    // 定义要写入W25Q128的数据数组,包含4个字节的数据
    uint8_t write_data[] = {0x01, 0x02, 0x03, 0x04};

    // 定义一个数组用于存储从W25Q128读取的数据,大小为4个字节
    uint8_t read_data[4];

    // 使用软件模拟SPI的方式擦除W25Q128的0x000000扇区,
    // 擦除操作会将该扇区的数据全部置为0xFF
    W25Q128_SectorErase(0x000000);

    // 使用软件模拟SPI的方式将write_data数组中的数据写入W25Q128的0x000000地址,
    // 写入数据长度为4个字节
    W25Q128_PageProgram(0x000000, write_data, 4);

    // 使用软件模拟SPI的方式从W25Q128的0x000000地址读取4个字节的数据到read_data数组中
    W25Q128_ReadData(0x000000, read_data, 4);

    // 使用硬件SPI的方式擦除W25Q128的0x000000扇区
    W25Q128_SectorErase_HardwareSPI(0x000000);

    // 使用硬件SPI的方式将write_data数组中的数据写入W25Q128的0x000000地址,
    // 写入数据长度为4个字节
    W25Q128_PageProgram_HardwareSPI(0x000000, write_data, 4);

    // 使用硬件SPI的方式从W25Q128的0x000000地址读取4个字节的数据到read_data数组中
    W25Q128_ReadData_HardwareSPI(0x000000, read_data, 4);

    // 进入一个无限循环,程序会一直停留在这个循环中,
    // 可以在此处添加其他需要持续运行的代码逻辑
    while (1) {
    }
}

6. 总结

本文详细介绍了基于 STM32F407 HAL 库实现软件模拟 SPI 和硬件 SPI 读写 W25Q128 的方法。软件模拟 SPI 具有灵活性高、无需特定硬件支持的优点,但通信速度相对较慢;硬件 SPI 则具有通信速度快、稳定性高的特点,但需要使用特定的硬件资源。在实际应用中,可根据具体需求选择合适的 SPI 通信方式。

7. 注意事项

  • 在使用软件模拟 SPI 时,要注意时钟信号的产生和数据的发送与接收顺序,确保通信的正确性。
  • 在使用硬件 SPI 时,要正确配置 SPI 外设的参数,如时钟极性、时钟相位、数据位宽等。
  • 在对 W25Q128 进行写操作之前,需要先进行写使能操作,并且在擦除扇区和写入数据时要注意地址的正确性。

以上内容详细介绍了基于 STM32F407 HAL 库软件模拟 SPI 读写 W25Q128 与硬件 SPI 读写 W25Q128 的实现方法,你可以根据实际需求进行调整和扩展,有什么不懂的可以留言私信。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值