一、SPI简介
SPI(Serial Peripheral Interface) 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。
芯片的管脚上只占用四根线。
MISO: 主器件数据输出,从器件数据输入。
MOSI:主器件数据输入,从器件数据输出。
SCK: 时钟信号,由主设备控制发出。
NSS(CS): 从设备选择信号,由主设备控制。当NSS为低电平则选中从器件。
二、引脚分布
STM32 芯片有多个 SPI 外设,它们的 SPI 通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚。其中 SPI1 是 APB2 上的设备,最高通信速率达 36Mbtis/s,SPI2、SPI3 是 APB1 上的设备,最高通信速率为 18Mbits/s。除了通讯速率,在其它功能上没有差异。其中 SPI3 用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是 IO 口,如果想使用 SPI3 接口,则程序上必须先禁用掉这几个 IO 口的下载功能。一般在资源不是十分紧张的情况下,这几个 IO 口是专门用于下载和调试程序,不会复用为 SPI3。
三、FLASH芯片
开发板中的 FLASH 芯片型号:W25Q64。W25Q 系列为台湾华邦公司推出的是一种使用 SPI 通讯协议的 NOR FLASH 存储器。芯片型号后两位表示芯片容量,例如 W25Q64 的 64 就是指 64Mbit 也就是 8M 的容量。它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SPI 引脚 NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚虽然是其片上 SPI 外设的硬件引脚,但实际上后面的程序只是把它当成一个普通的 GPIO,使用软件的方式控制 NSS 信号,所以在 SPI 的硬件设计中,NSS 可以随便选择普通的 GPIO,不必纠结于选择硬件 NSS 信号。
FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。
通过控制 STM32 利用 SPI 总线向 FLASH 芯片发送指令,FLASH 芯片收到后就会执行相应的操作。
主机首先通过 MOSI 线向 FLASH 芯片发送第一个字节数据为“9F h”,当 FLASH 芯片收到该数据后,它会解读成主机向它发送了“JEDEC 指令”,然后它就作出该命令的响应:通过 MISO 线把它的厂商 ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备 ID 来测试硬件是否连接正常,或用于识别设备。
对于 FLASH 芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多的数据。
实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。如上一章的 EEPROM 使用的是 I2C,本章的 FLASH 使用的是 SPI。那么我们就先根据它的通讯协议,选择好 STM32 的硬件模块,并进行相应的 I2C 或 SPI 模块初始化。接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如 EEPROM 中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而 FLASH 则定义了更多的指令,有写指令,读指令,读 ID 指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标。
四、新建工程
1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟
RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器)
选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz
修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式
非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器
SYS 设置,选择 Debug 为 Serial Wire
五、SPI1
5.1 参数配置
在 Connectivity
中选择 SPI1
设置,并选择 Full-Duplex Master
全双工主模式,不开启 NSS
即不使用硬件片选信号
原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PA4 配置为普通 GPIO,手动控制片选信号。
在右边图中找到 SPI1 NSS 对应引脚,选择 GPIO_Output
。纠正:野火STM32F103指南者开发板SPI1 NSS须配置为PC0
修改输出高电平 High
,标签为 W25Q64_CHIP_SELECT
。
SPI 为默认设置不作修改。只需注意一下,Prescaler
分频系数最低为 4
,波特率 (Baud Rate) 为 18.0 MBits/s
。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s。
- Clock Polarity(CPOL):SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
- Clock Phase(CPHA):指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。
根据 FLASH 芯片的说明,它支持 SPI模式0
及模式 3
,支持双线全双工,使用 MSB 先行模式,数据帧长度为 8 位。
所以这里配置 CPOL 为Low
,CPHA 为1 Edge
即 SPI模式0
。
5.2 生成代码
输入项目名和项目路径
选择应用的 IDE 开发环境 MDK-ARM V5
每个外设生成独立的 ’.c/.h’
文件
不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。
点击 GENERATE CODE 生成代码
5.3 封装SPI Flash(W25Q64)的命令和底层函数
- 向 SPI Flash 发送数据的函数
static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)
{
return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);
}
- 从 SPI Flash 接收数据的函数
static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)
{
return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);
}
- 发送数据的同时读取数据的函数
static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)
{
return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);
}
5.4 编写W25Q64的驱动程序
5.4.1 读取 Manufacture ID 和 Device ID
读取 Flash 内部这两个 ID 有两个作用:
- 检测 SPI Flash 是否存在
- 可以根据 ID 判断 Flash 具体型号
uint16_t W25QXX_ReadID(void)
{
uint8_t recv_buf[2] = { 0}; //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID
uint16_t device_id = 0;
uint8_t send_data[4] = { ManufactDeviceID_CMD,0x00,0x00,0x00}; //待发送数据,命令+地址
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
if (HAL_OK == SPI_Transmit(send_data, 4))
{
if (HAL_OK == SPI_Receive(recv_buf, 2))
{
device_id = (recv_buf[0] << 8) | recv_buf[1];
}
}
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return device_id;
}
5.4.2 读取状态寄存器数据并判断Flash是否忙碌
SPI Flash 的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有 2-3 个状态寄存器,指示出 Flash 当前的状态,有趣的一点是:
当 Flash 内部在执行命令时,不能再执行 MCU 发来的命令,但是 MCU 可以一直读取状态寄存器,这下就很好办了,MCU可以一直读取,然后判断 Flash 是否忙完。
static uint8_t W25QXX_ReadSR(uint8_t reg)
{
uint8_t result = 0;
uint8_t send_buf[4] = { 0x00,0x00,0x00,0x00};
switch(reg)
{
case 1:
send_buf[0] = READ_STATU_REGISTER_1;
case 2:
send_buf[0] = READ_STATU_REGISTER_2;
case 0:
default:
send_buf[0] = READ_STATU_REGISTER_1;
}
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
if (HAL_OK == SPI_Transmit(send_buf, 4))
{
if (HAL_OK == SPI_Receive(&result, 1))
{
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return result;
}
}
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return 0;
}
然后编写阻塞判断 Flash 是否忙碌的函数:
static void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}
5.4.3 读取数据
SPI Flash 读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制。
int W25QXX_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes)
{
uint8_t cmd = READ_DATA_CMD;
start_addr = start_addr << 8;
W25QXX_Wait_Busy();
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
if (HAL_OK == SPI_Transmit((uint8_t*)&start_addr, 3))
{
if (HAL_OK == SPI_Receive(buffer, nbytes))
{
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return 0;
}
}
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
return -1;
}
5.4.4 写使能/禁止
Flash 芯片默认禁止写数据,所以在向 Flash 写数据之前,必须发送命令开启写使能。
void W25QXX_Write_Enable(void)
{
uint8_t cmd= WRITE_ENABLE_CMD;
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
W25QXX_Wait_Busy();
}
void W25QXX_Write_Disable(void)
{
uint8_t cmd = WRITE_DISABLE_CMD;
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
W25QXX_Wait_Busy();
}
5.4.5 擦除扇区
SPI Flash有个特性:
数据位可以由1变为0,但是不能由0变为1。
所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF
(即全为1)。
void W25QXX_Erase_Sector(uint32_t sector_addr)
{
uint8_t cmd = SECTOR_ERASE_CMD;
sector_addr *= 4096; //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址
sector_addr <<= 8;
W25QXX_Write_Enable(); //擦除操作即写入0xFF,需要开启写使能
W25QXX_Wait_Busy(); //等待写使能完成
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
SPI_Transmit((uint8_t*)§or_addr, 3);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
W25QXX_Wait_Busy(); //等待扇区擦除完成
}
5.4.6 页写入操作
向 Flash 芯片写数据的时候,因为 Flash 内部的构造,可以按页写入。
void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes)
{
uint8_t cmd = PAGE_PROGRAM_CMD;
WriteAddr <<= 8;
W25QXX_Write_Enable();
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
SPI_Transmit((uint8_t*)&WriteAddr, 3);
SPI_Transmit(dat, nbytes);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
W25QXX_Wait_Busy();
}
5.5 添加宏定义和全局变量
#define ManufactDeviceID_CMD 0x90
#define READ_STATU_REGISTER_1 0x05
#define READ_STATU_REGISTER_2 0x35
#define READ_DATA_CMD 0x03
#define WRITE_ENABLE_CMD 0x06
#define WRITE_DISABLE_CMD 0x04
#define SECTOR_ERASE_CMD 0x20
#define CHIP_ERASE_CMD 0xc7
#define PAGE_PROGRAM_CMD 0x02
SPI_HandleTypeDef hspi1;
UART_HandleTypeDef huart1;
uint16_t device_id;
uint8_t read_buf[10] = { 0};
uint8_t write_buf[10] = { 0};
int i;
5.6 添加测试函数
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_SPI1_Init();
device_id = W25QXX_ReadID();
printf("W25Q64 Device ID is 0x%04x\r\n", device_id);
printf("-------- read data before write -----------\r\n");
W25QXX_Read(read_buf, 0, 10);
for(i = 0; i < 10; i++)
{
printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
}
printf("-------- erase sector 0 -----------\r\n");
W25QXX_Erase_Sector(0);
printf("-------- read data after erase -----------\r\n");
W25QXX_Read(read_buf, 0, 10);
for(i = 0; i < 10; i++)
{
printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
}
printf("-------- write data -----------\r\n");
for(i = 0; i < 10; i++)
{
write_buf[i] = i;
}
W25QXX_Page_Program(write_buf, 0, 10);
printf("-------- read data after write -----------\r\n");
W25QXX_Read(read_buf, 0, 10);
for(i = 0; i < 10; i++)
{
printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i));
}
while (1)
{
}
}
5.7 查看打印
串口打印功能查看 STM32CubeMX学习笔记(6)——USART串口使用
5.8 HAL库与标准库代码比较
STM32CubeMX 使用 HAL 库生成的代码:
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = { 0};
__HAL_RCC_GPIOC_CLK_ENABLE();
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
GPIO_InitStruct.Pin = W25Q64_CHIP_SELECT_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(W25Q64_CHIP_SELECT_GPIO_Port, &GPIO_InitStruct);
}
static void MX_SPI1_Init(void)
{
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_4;
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();
}
}
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,
uint32_t Timeout);
使用 STM32 标准库的代码:
void SPI_FLASH_Init(void)
{
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
FLASH_SPI_CS_APBxClock_FUN (FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|
FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);
SPI_FLASH_CS_HIGH();
// FLASH芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(FLASH_SPIx , &SPI_InitStructure);
SPI_Cmd(FLASH_SPIx , ENABLE);
}
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
六、注意事项
用户代码要加在 USER CODE BEGIN N
和 USER CODE END N
之间,否则下次使用 STM32CubeMX 重新生成代码后,会被删除。
• 由 Leung 写于 2021 年 1 月 27 日
• 参考:STM32CubeMX系列教程10:串行外设接口SPI(一)
STM32CubeMX系列教程11:串行外设接口SPI(二)
STM32CubeMX | 30-使用硬件SPI读写FLASH(W25Q64)