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

发布于:2025-05-29 ⋅ 阅读:(21) ⋅ 点赞:(0)

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) CLK SPI 时钟信号
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 的实现方法,你可以根据实际需求进行调整和扩展,有什么不懂的可以留言私信。


网站公告

今日签到

点亮在社区的每一天
去签到