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_PINGPIO_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用于存储接收到的字节数据,初始化为0uint8_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) {// 用于存储读取到的设备IDuint16_t device_id;// 拉低片选信号,选中W25Q128HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);// 发送读取设备ID的命令码0x90SPI_SendByte(0x90);// 发送地址字节,这里地址为0x000000SPI_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位设备IDdevice_id |= SPI_SendByte(0xFF);// 拉高片选信号,取消选中W25Q128HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_SET);// 返回读取到的设备IDreturn device_id;
}/*** @brief 使能W25Q128的写操作* * 该函数通过SPI接口向W25Q128发送写使能命令,允许后续的写操作。*/
void W25Q128_WriteEnable(void) {// 拉低片选信号,选中W25Q128HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);// 发送写使能命令码0x06SPI_SendByte(0x06);// 拉高片选信号,取消选中W25Q128HAL_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();// 拉低片选信号,选中W25Q128HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);// 发送扇区擦除命令码0x20SPI_SendByte(0x20);// 发送扇区地址的高8位SPI_SendByte((sector_address >> 16) & 0xFF);// 发送扇区地址的中间8位SPI_SendByte((sector_address >> 8) & 0xFF);// 发送扇区地址的低8位SPI_SendByte(sector_address & 0xFF);// 拉高片选信号,取消选中W25Q128HAL_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();// 拉低片选信号,选中W25Q128HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);// 发送页编程命令码0x02SPI_SendByte(0x02);// 发送写入地址的高8位SPI_SendByte((address >> 16) & 0xFF);// 发送写入地址的中间8位SPI_SendByte((address >> 8) & 0xFF);// 发送写入地址的低8位SPI_SendByte(address & 0xFF);// 循环将数据逐字节写入W25Q128for (uint16_t i = 0; i < length; i++) {SPI_SendByte(data[i]);}// 拉高片选信号,取消选中W25Q128HAL_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) {// 拉低片选信号,选中W25Q128HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_NSS_PIN, GPIO_PIN_RESET);// 发送读取数据命令码0x03SPI_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);}// 拉高片选信号,取消选中W25Q128HAL_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外设实例为SPI1hspi1.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外设实例是否为SPI1if(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_4GPIO_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};// 定义接收数据的数组,用于存储读取到的设备IDuint8_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位的设备IDreturn (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扇区,// 擦除操作会将该扇区的数据全部置为0xFFW25Q128_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 的实现方法,你可以根据实际需求进行调整和扩展,有什么不懂的可以留言私信。