基于STM32_HAL库的SPI通信并驱动W25Q64存储模块
基于STM32_HAL库的SPI通信并驱动W25Q64存储模块
文章目录
- 基于STM32_HAL库的SPI通信并驱动W25Q64存储模块
- 一、SPI总线概述
- 1.1 IIC总线&SPI总线对比
- 1.2 SPI总线结构
- 1.3 SPI总线传输实例
- 1.4 SPI的波特率
- 1.5 SPI比特位传输顺序(MSB First / LSB First)
- 1.6 SPI数据位长度(8位/16位)
- 1.7 SPI时钟的极性
- 1.8 SPI时钟的相位
- 1.9 SPI时钟的四种模式
- 二、STM32_HAL库 SPI相关API函数
- 2.1 初始化SPI外设函数`HAL_SPI_Init()`原型及功能
- 2.2 阻塞式发送数据函数`HAL_SPI_Transmit()`原型及功能
- 2.3 阻塞式接收数据函数`HAL_SPI_Receive()`原型及功能
- 2.4 阻塞式同时发送和接收数据函数`HAL_SPI_TransmitReceive()`原型及功能
- 三、W25Q64 FALSH模块概述
- 3.1 核心参数
- 3.2 引脚定义
- 3.3 存储结构层级(从大到小)
- 3.4 W25Q64通信协议
- 3.5 W25Q64常用指令码
- 四、STM32硬件SPI实现W25Q64 FLASH模块通信
- 4.1 硬件连接
- 4.2 CubeMX配置
- 4.3 代码实现
一、SPI总线概述
SPI(Serial Peripheral Interface,串行外设接口)是一种同步串行通信协议,用于微控制器与外部设备之间的高速数据交换。它由 Motorola 公司提出,广泛应用于嵌入式系统中。SPI 是一种让主设备和一个或多个从设备通过几根线快速传数据的通信方式。
1.1 IIC总线&SPI总线对比
I²C(Inter-Integrated Circuit)总线和 SPI(Serial Peripheral Interface)总线是两种常用的串行通信接口,它们在嵌入式系统中有广泛应用。下面从结构、速率、引脚数量、通信方式等方面对两者进行详细对比:
🧩 I²C 与 SPI 总线对比表
对比项目 | I²C 总线 | SPI 总线 |
---|---|---|
通信方式 | 主从式,支持多主多从 | 主从式,典型为一主多从 |
引脚数量 | 2 根(SCL、SDA) | 通常 4 根(SCLK、MOSI、MISO、CS) |
硬件资源 | 占用少,硬件接口简单 | 占用引脚多(每个从设备需要独立 CS) |
传输速率 | 标准模式:100kbps 快速模式:400kbps 高速模式:3.4Mbps | 常见可达 10Mbps 以上,部分可超 100Mbps |
传输效率 | 较低(起始/停止位、ACK/NACK 消耗带宽) | 高(纯数据传输,无冗余) |
主从识别方式 | 地址寻址(7位/10位) | 片选信号(CS)识别从机 |
双向通信 | 支持半双工 | 支持全双工 |
电气连接 | 支持多设备共享总线(并联) | 每个从机需要单独 CS 线 |
协议复杂度 | 高,需要状态管理(ACK、START/STOP等) | 低,纯同步传输 |
适用场合 | 小容量器件、低速外设(EEPROM、RTC、传感器等) | 高速设备、大数据传输(Flash、LCD、ADC等) |
长距离通信 | 不适合(噪声易影响) | 一般也不适合,但比 I²C 略好 |
硬件支持 | 广泛(许多器件仅支持 I²C) | 较多,但不如 I²C 普及 |
1.2 SPI总线结构
- 单片机一般作为SPI总线的主机,其他模块作为从机,单片机可以通过SPI总线可以和大量的模块进行连接,SPI总线总共有4条线组成,分别是:MOSI、MISO、SCK、NSS下面我们逐一的来认识一下这几条线:
- MOSI是Master Output Slave Input的缩写,所以它的意思就是:主机发送从机接收,因此对于主机来说它是数据发送引脚,对于从机来说它是数据接收引脚,在接线的时候我们应该将主机的MOSI和所有从机的MOSI连接在一起,主机通过这条线向从机发送数据,规则是低电压表示0 高电压表示1。
- MISO是Master Input Slave Output的缩写,所以它的意思就是:主机接收从机发送,在接线的时候我们应该将主机的MISO和所有从机的MISO相连,主机通过这条线从从机接收数据,而且传输的规则也是低电压表示0 高电压表示1。
- SCK是Serial Clock的缩写,代表串行时钟线,在接线的时候我们应该把主机的SCK和所有从机的SCK连接在一起,在传输数据的过程当中时钟信号由主机产生并通过SCK发送给从机,而且每个时钟周期发送一个位,因此时钟的频率就决定了通信速率的快慢,时钟频率越高数据传输速度就越快。
- NSS是Negative Slave Select的缩写,所以这是一条:低电压有效的从机选择线,主机向对应从机的NSS引脚发送一个低电压就可以选择对应的从机,在实际接线的时候我们应该将主机的NSS和对应从机的NSS相连,每一个从机就有一个NSS,所以有多少个从机就有多少条NSS,这样主机想要向哪个从机通信就向哪个从机的NSS发送一个低电压。
1.3 SPI总线传输实例
我们打算让主机和从机1进行通信,主机通过SPI总线向从机1发送一个字节的数据,假设我们要发送的这个数据是10进制的100,但是因为SPI总线的通信方向是双向的,所以我们在发送这个字节的时候也会从从机收到这个数据,因为我们要和从机1通信所以我们在通信的过程中要选中从机1,我们向NSS1引脚输出一个低电平,通信结束之后再回复高电压,而从机2和从机3我们用不到,所以我们給NSS2和NSS3高电压。
在通信的过程当中主机产生时钟信号并通过SCK发送出去,时钟信号就是像上面图中的方波。
MOSI表示主机发送从机接受,主机通过这条线向从机发送数据,每个时钟周期发送一个位,低电压表示0 高电压表示1,我们要发送的数据是十进制的100,先把它转换成二进制也就是:0110 0100,然后我们遇到0就发送低电压,遇到1就发送高电压,这样我们就把数据发送出去了。
MISO代表主机接收从机发送,主机通过这条线从从机接收数据,由于SPI通信的总线是双向的,所以主机向从机发送数据的同时也会从从机接收数据,发送多少个字节就接收多少个字节,假设我们从从机接收的数据是:1100 1010转换成十六进制就是0xCA。
1.4 SPI的波特率
SPI总线通过这种高低变化的电压来传输数据,低电压表示0 高电压表示1,每个时钟周期传输一个二进制的bit,因此我们将每秒钟传输高低电压的数量叫做波特率,波特率越大数据传输的速度就越快,SPI总线没有规定波特率的取值范围,但是波特率一般会取几兆——几十兆HZ之间。
SPI波特率选取原则:
- 选取允许的最大值
- 考虑设备所能承受的极限
- 考虑电路板所能承受的极限
1.5 SPI比特位传输顺序(MSB First / LSB First)
计算机使用二进制来表示数字,对于一个无符号8位整数来说,它有8bit位来组成,分别是bit0—bit7,每一个bit位拥有不同的权重,bit0的权重最低代表LSB,bit7的权重最高代表MSB。
LSB是Least Significant Bit的缩写,代表:最低有效位;MSB是Most Significant Bit的缩写,代表:最高有效位。
这样我们就有了两种传输方式,分别是:MSB First 先传最高有效位、LSB First 先传最低有效位。
假设我们同样都是要传输十进制99,先把它转换成二进制就是0110 0011;最左边的0代表最高有效位 为 MSB,最右边的1代表最低有效位 为 LSB
如果使用MSB First方式传输:那么应该先发送MSB 后发LSB,也就是高位在前 低位在后;如果使用LSB First方式传输:那么应该先发送LSB 后发MSB,也就是低位在前 高位在后。
在实际的项目中是选择MSB First|LSB First传输数据,要根据实际情况做决定。
1.6 SPI数据位长度(8位/16位)
- 数据位的长度分为:8-bit 和 16-bit:
- 其中8-bit代表:在数据传输的过程中以8个bit位作为一组,每组传输一个字节。
- 其中16-bit代表:在数据传输的过程中以16个bit位作为一组,每组传输一个整数。
举例:我们想要通过SPI总线去传输一个16位的整数:0xb51f,并且要求使用:MSB First的方式传输
8-bit发送方式:如果使用8-bit,我们需要分两次传输,首先我们将0xb51f拆分成两个字节(0xb5,0x1f),首先发送0xb5,把0xb5转换成二进制1011 0101,先发MSB后发LSB,然后我们再发送0x1f,把0x1f也转换成二进制0001 1111,先发MSB后发LSB。
16-bit发送方式:我们可以直接将0xb51f转换成16位二进制数:1011 0101 0001 1111,最左边的1是MSB,最右边的1是LSB。
我们对比两次发送的波形完全一致,但是发送的过程却有所区别。
在实际的项目中选择8-bit还是16-bit的发送方式要根据情况来定,一般没有特殊要求选择8-bit发送方式就可以了。
1.7 SPI时钟的极性
-
时钟信号的极性分为两种,分为:低极性 和 高极性。
-
低极性:如果选择时钟信号位低极性,表示在空闲状态下时钟信号的电压是低电压。
-
高极性:如果选择时钟信号位高极性,表示在空闲状态下时钟信号的电压是高电压。
-
第一边沿:在时钟信号为低极性的情况下,所有的上升沿是第一边沿;在时钟信号为高极性的情况下,所有的下升沿是第一边沿。
-
第二边沿:在时钟信号为低极性的情况下,所有的下降沿是第二边沿;在时钟信号为高极性的情况下,所有的上升沿是第二边沿。
1.8 SPI时钟的相位
主机通过SCK线向从机发送时钟信号,每个时钟周期MOSI和MISO上各会传输一个二进制比特位,低电压表示0,高电压表示1。
然后我们从图中截取一段波形把它放大,可以发现比特位的传输可以分成两个阶段,分别是:发送阶段 和 采集阶段。
- 发送阶段:在发送阶段如果想要发送一个1,那么发送方就把电压拉高,如果想要发送一个0,那么发送方就把电压拉低。
- 采集阶段:当发送方把数据准备好之后就会进入第二个阶段,在采集阶段发送方维持信号不变,等待接收方读取数据。
-
时钟的相位有两种,分别是:第一边沿采集 和 第二边沿采集。
-
第一边沿采集:如果接收方在第一边沿采集信号,我们把这种方式叫做第一边沿采集。
-
第二边沿采集:如果接收方在第二边沿采集信号,我们把这种方式叫做第二边沿采集。
-
第一边沿采集 和 第二边沿采集分别叫做时钟的两种相位。
1.9 SPI时钟的四种模式
时钟的极性分为:**低极性(0)**和 高极性(1)。
时钟的相位分为:**第一边沿采集(0)**和 第二边沿采集(1)。
通过时钟的两种极性 和 时钟的两种相位,于是我们引出了时钟的四种模式:模式0、模式1、模式2、模式3。
模式0:模式0代表低极性,第一边沿采集。
模式1:模式1代表低极性,第二边沿采集。
模式2:模式2代表高极性,第一边沿采集。
模式3:模式3代表高极性,第二边沿采集。
二、STM32_HAL库 SPI相关API函数
2.1 初始化SPI外设函数HAL_SPI_Init()
原型及功能
🔹 函数原型:
HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
🔹 参数说明:
hspi
:SPI 句柄,包含 SPI 寄存器基址和配置。
🔹 返回值:
HAL_OK
:成功HAL_ERROR
:失败
🔹 功能:
初始化 SPI 外设,需在使用前调用一次。
2.2 阻塞式发送数据函数HAL_SPI_Transmit()
原型及功能
🔹 函数原型:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
🔹 参数说明:
hspi
:SPI 句柄pData
:待发送数据缓冲区Size
:发送字节数Timeout
:超时时间(ms)
🔹 返回值:
HAL_OK
:成功HAL_BUSY
:设备忙HAL_TIMEOUT
:超时HAL_ERROR
:错误
🔹 功能:
阻塞发送数据,直到全部传完或超时。
2.3 阻塞式接收数据函数HAL_SPI_Receive()
原型及功能
🔹 函数原型:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
🔹 参数说明:
hspi
:SPI 句柄pData
:接收数据的缓冲区指针Size
:要接收的数据字节数Timeout
:超时时间(ms)
🔹 返回值:
HAL_OK
:接收成功HAL_BUSY
:设备忙HAL_TIMEOUT
:超时HAL_ERROR
:出错(配置错误、通信异常)
🔹 功能:
阻塞接收数据,主设备或从设备均可用。
2.4 阻塞式同时发送和接收数据函数HAL_SPI_TransmitReceive()
原型及功能
🔹 函数原型:
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,uint8_t *pTxData,uint8_t *pRxData,uint16_t Size,uint32_t Timeout);
🔹 参数说明:
hspi
:SPI 句柄pTxData
:待发送数据缓冲区指针pRxData
:接收数据缓冲区指针Size
:要发送和接收的数据字节数Timeout
:超时时间(ms)
🔹 返回值:
HAL_OK
:接收成功HAL_BUSY
:设备忙HAL_TIMEOUT
:超时HAL_ERROR
:出错(配置错误、通信异常)
🔹 功能:
该函数实现 阻塞式 SPI 全双工通信,即在传输 Size
字节的同时,接收同样数量的数据。
三、W25Q64 FALSH模块概述
W25Q64 是 华邦电子(Winbond) 出品的一款 串行 Flash 存储芯片,容量为 64Mbit(8MB),支持 SPI 接口,常用于固件存储、数据记录、启动代码等应用。
型号全称:W25Q64JV(JV表示版本,比如 W25Q64JVSSIQ)
3.1 核心参数
项目 | 参数值 |
---|---|
容量 | 64 Mbit(8 MB) |
接口类型 | SPI(标准 SPI / Dual SPI / Quad SPI) |
支持电压 | 2.7V - 3.6V(典型值 3.3V) |
最大 SPI 频率 | 104 MHz(标准 SPI) |
擦除单位 | 4KB(Sector)、32KB/64KB Block、全片 |
写入单位 | 1-256字节(页写) |
数据保持 | >20年 |
擦写次数 | >100,000次 |
工作温度范围 | -40°C ~ +85°C(工业级) |
3.2 引脚定义
引脚编号 | 名称 | 功能说明 |
---|---|---|
1 | /CS | 片选(低电平有效) |
2 | DO (MISO) | 主机读取数据线 |
3 | /WP | 写保护(低电平启用保护) |
4 | GND | 地 |
5 | DI (MOSI) | 主机发送数据线 |
6 | CLK | SPI 时钟 |
7 | /HOLD | 暂停功能(低电平暂停) |
8 | VCC | 电源(3.3V) |
通常只需要接 CS, CLK, MOSI, MISO, VCC 和 GND,WP 和 HOLD 可拉高(禁用)
3.3 存储结构层级(从大到小)
W25Q64 的内部存储结构是分层组织的:
层级 | 数量 | 大小 | 总容量 |
---|---|---|---|
芯片 | 1 | 8 MB | — |
块(Block) | 128 | 64 KB | 128 × 64KB = 8MB |
扇区(Sector) | 2048 | 4 KB | 2048 × 4KB = 8MB |
页(Page) | 32768 | 256 Byte | 32768 × 256B = 8MB |
3.4 W25Q64通信协议
W25Q64 使用 SPI 总线进行主从通信,主机(如 STM32)通过发送命令字节开始交互。
✅ 支持的 SPI 模式:
- 模式 0(CPOL=0,CPHA=0)
- 模式 3(CPOL=1,CPHA=1)
STM32 使用 HAL 库配置 SPI 时,设置为
SPI_MODE_MASTER
、SPI_POLARITY_LOW
、SPI_PHASE_1EDGE
(即模式0)
3.5 W25Q64常用指令码
操作 | 指令码 | 说明 |
---|---|---|
读取设备ID | 0x90 | 读取制造商ID/设备ID(需发送3字节地址) |
读取 JEDEC ID | 0x9F | 读取3字节 JEDEC ID(厂商ID + 类型ID + 容量ID) |
读数据 | 0x03 | 普通读取(最常用) |
快速读 | 0x0B | 支持高速读取(多用于 Quad SPI) |
页编程(写) | 0x02 | 写入 1~256 字节数据 |
写使能 | 0x06 | 所有写操作前必须执行 |
读状态寄存器1 | 0x05 | 常用于检测 BUSY 状态 |
擦除 4KB Sector | 0x20 | 擦除一个 Sector(4KB) |
擦除整片 | 0xC7 | 擦除整个芯片 |
片选控制 | /CS | 每条指令都需在 CS 拉低后发出,结束后拉高 |
四、STM32硬件SPI实现W25Q64 FLASH模块通信
项目需求:每按一次按键切换一下LED状态,把LED的状态存储到FLASH模块里面,系统每一次上电都会读取存储在FLASH模块LED的状态并显示。
4.1 硬件连接
4.2 CubeMX配置
-
配置Debug
-
打开RCC配置时钟,将高速时钟配置成Crystal/Ceramic Resonator
-
点击配置时钟选项,把时钟配置成高速外部时钟HSE,并且通过PLLCLK倍频到72MHZ,按下回车之后就为芯片内部的功能分配好了对应的时钟频率。
-
将PB8配置成GPIO_Output模式,PB8连接的是LED,然后设置初始电平为高电平,模式为推挽输出,无上拉和下拉电阻,输出的速度设置是为:低速。
-
将PA0配置成GPIO_Input模式,PA0连接的是按键,因为在硬件上已经加了上拉电阻,所以这里我们可以选择:无上拉下拉电阻。
-
选择左侧的SPI1选项,然后将SPI模式配置成:全双工标准的双向同时通信,然后下面就会出现一个硬件NSS选项,这里我们选择不使用硬件NSS信号,硬件NSS主要用于多主机通信。
-
然后我对我们的参数进行设置:首先配置数据帧格式为:摩托罗拉格式,数据为长度为:8bit,数据为传输顺序:MSB First先传最高有效位,波特率1.125MBits/s是根据分频系数进行配置的,时钟的极性选择为:Low低极性,时钟的相位选择为:第一边沿采集,根据时钟的极性和相位所以我们选择的是SPI的模式3,高级参数不需要配置。
-
配置PA4为GPIO_Outpt模式,初始电平为:高电平,GPIO模式:推挽输出模式,GPIO速度:高速模式,PA4连接的是W25Q64模块的NSS引脚,向NSS引脚输出一个低电压代表选择从机。
-
接着对我们的工程进行配置,配置完成之后点击右上角的 GENERATE CODE 生成工程
4.3 代码实现
在实现我们的业务代码之前,我们首先先阅读一下使用CubeMX生成的代码:
/* main.c */
void SystemClock_Config(void)
{RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};/** Initializes the RCC Oscillators according to the specified parameters* in the RCC_OscInitTypeDef structure.*/RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEState = RCC_HSE_ON;RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){Error_Handler();}/** Initializes the CPU, AHB and APB buses clocks*/RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK){Error_Handler();}
}
上面这段代码是使用CubeMX生成的时钟相关的代码。
/* gpio.c */
void MX_GPIO_Init(void)
{GPIO_InitTypeDef GPIO_InitStruct = {0};/* GPIO Ports Clock Enable */__HAL_RCC_GPIOD_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);/*Configure GPIO pin : PA0 */GPIO_InitStruct.Pin = GPIO_PIN_0;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/*Configure GPIO pin : PA4 */GPIO_InitStruct.Pin = GPIO_PIN_4;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/*Configure GPIO pin : PB8 */GPIO_InitStruct.Pin = GPIO_PIN_8;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);}
上面这段代码是使用CubeMX生成的GPIO初始化相关的代码,对PA0、PA4、PB8进行了初始化配置。
/* spi.c */
#include "spi.h"/* USER CODE BEGIN 0 *//* USER CODE END 0 */SPI_HandleTypeDef hspi1;/* SPI1 init function */
void MX_SPI1_Init(void)
{/* USER CODE BEGIN SPI1_Init 0 *//* USER CODE END SPI1_Init 0 *//* USER CODE BEGIN SPI1_Init 1 *//* USER CODE END SPI1_Init 1 */hspi1.Instance = SPI1;hspi1.Init.Mode = SPI_MODE_MASTER;hspi1.Init.Direction = SPI_DIRECTION_2LINES;hspi1.Init.DataSize = SPI_DATASIZE_8BIT;hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;hspi1.Init.NSS = SPI_NSS_SOFT;hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64;hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;hspi1.Init.TIMode = SPI_TIMODE_DISABLE;hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;hspi1.Init.CRCPolynomial = 10;if (HAL_SPI_Init(&hspi1) != HAL_OK){Error_Handler();}/* USER CODE BEGIN SPI1_Init 2 *//* USER CODE END SPI1_Init 2 */}void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{GPIO_InitTypeDef GPIO_InitStruct = {0};if(spiHandle->Instance==SPI1){/* USER CODE BEGIN SPI1_MspInit 0 *//* USER CODE END SPI1_MspInit 0 *//* SPI1 clock enable */__HAL_RCC_SPI1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**SPI1 GPIO ConfigurationPA5 ------> SPI1_SCKPA6 ------> SPI1_MISOPA7 ------> SPI1_MOSI*/GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);GPIO_InitStruct.Pin = GPIO_PIN_6;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* USER CODE BEGIN SPI1_MspInit 1 *//* USER CODE END SPI1_MspInit 1 */}
}void HAL_SPI_MspDeInit(SPI_HandleTypeDef* spiHandle)
{if(spiHandle->Instance==SPI1){/* USER CODE BEGIN SPI1_MspDeInit 0 *//* USER CODE END SPI1_MspDeInit 0 *//* Peripheral clock disable */__HAL_RCC_SPI1_CLK_DISABLE();/**SPI1 GPIO ConfigurationPA5 ------> SPI1_SCKPA6 ------> SPI1_MISOPA7 ------> SPI1_MOSI*/HAL_GPIO_DeInit(GPIOA, GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7);/* USER CODE BEGIN SPI1_MspDeInit 1 *//* USER CODE END SPI1_MspDeInit 1 */}
}
上面这段代码是使用CubeMX生成的SPI1相关的初始化代码。
/* main.c */
#include "main.h"
#include "spi.h"
#include "gpio.h"/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
uint8_t ledState = 0;
/* USER CODE END PM *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP *//* USER CODE END PFP *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 *//* 上电从FLASH里读取LED状态 */
static uint8_t LoadLEDState(void)
{uint8_t readDataCmd[] = {0x03, 0x00, 0x00, 0x00};uint8_t ledState = 0xff;HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 读取之前拉低从机NSSHAL_SPI_Transmit(&hspi1, readDataCmd, sizeof(readDataCmd) / sizeof(readDataCmd[0]), HAL_MAX_DELAY); // 向从机发送读数据指令HAL_SPI_Receive(&hspi1, &ledState, 1, HAL_MAX_DELAY); // 主机接收来自从机的LED状态HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 读取完成拉高从机NSSreturn ledState;
}/* 向从机写入LED状态 */
static void SaveLEDState(uint8_t ledstate)
{// #1. 写使能uint8_t wirteEnableCmd[] = {0x06};HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, wirteEnableCmd, sizeof(wirteEnableCmd) / sizeof(wirteEnableCmd[0]), HAL_MAX_DELAY);// 向从机发送使能指令HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);// #2. 扇区擦除uint8_t sectorErose[] = {0x20, 0x00, 0x00, 0x00};HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_SPI_Transmit(&hspi1, sectorErose, sizeof(sectorErose) / sizeof(sectorErose[0]), HAL_MAX_DELAY); // 向从机发送扇区擦除指令HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);HAL_Delay(100);// #3. 写使能HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_SPI_Transmit(&hspi1, wirteEnableCmd, sizeof(wirteEnableCmd), HAL_MAX_DELAY); // 向从机发送使能信号HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);// #4. 页编程uint8_t pageProgCmd[5];pageProgCmd[0] = 0x02; // 页编程指令pageProgCmd[1] = 0x00; // 24位地址pageProgCmd[2] = 0x00;pageProgCmd[3] = 0x00; pageProgCmd[4] = ledState; // 要写入的数据(LED状态) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);HAL_SPI_Transmit(&hspi1, pageProgCmd, sizeof(pageProgCmd) / sizeof(pageProgCmd[0]), HAL_MAX_DELAY); // 向从机发送要写入的数据HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);HAL_Delay(10);
}
/* USER CODE END 0 */int main(void)
{/* USER CODE BEGIN 1 */uint8_t pre = 1;uint8_t cur = 1;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI1_Init();/* USER CODE BEGIN 2 *//* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */ledState = LoadLEDState(); // 上电初始化读取从机LED状态if(ledState == 1){HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);}while (1){pre = cur; // 更新per的值if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET){ // 更新cur的值cur = 1;}else{cur = 0;}if(pre != cur){ // 检测到按键按下HAL_Delay(10);if(cur == 0){ // 按键按下状态}else{ // 检测按键松开 点亮|熄灭 LED 并把LED状态写入到FLASH从机if(ledState == 1){HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);ledState = 0;}else{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);ledState = 1;}SaveLEDState(ledState);}}/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
上面这段代码就是我们要实现的业务代码,实现了单片机通过SPI总线和FLASH模块进行通信,完成了我们的业务需求。