- SPI简介
STM32的SPI是一个串行外设接口。它允许STM32微控制器与其他设备(如传感器、存储器等)进行高速、全双工、同步的串行通信。通常包含SCLK(串行时钟)、MOSI(主设备输出/从设备输入Master Output Slave Input)、MISO(主设备输入/从设备输出Master Input Slave Output)和NSS/CS(片选信号Chip Select)这4条线,支持多个从设备连接到一个主设备上。
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。
2. SPI使用步骤
我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128)为例,学习SPI。
2.1 SPI时钟SCLK
SPI时钟特点主要包括:时钟速率、时钟极性和时钟相位三方面。
时钟速率
SPI总线上的主设备必须在通信开始时候配置并生成相应的时钟信号。从理论上讲,只要实际可行,时钟速率就可以是你想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。
时钟极性
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。
CKP可以配置为1或0,这意味着可根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。须参考设备的数据手册才能正确设置CKP和CKE。
CKP = 0:时钟空闲IDLE为低电平0;
CKP = 1:时钟空闲IDLE为高电平1。
时钟相位
根据硬件制造商的不同,时钟相位通常写为CKE或CPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;
CKE = 0:在时钟信号SCK的第一个跳变沿采样;
CKE = 1:在时钟信号SCK的第二个跳变沿采样。
2.2四种操作根据
SPI的时钟极性和时钟相位特性可以设置4种不同的SPI通信操作模式,它们的区别是定义了在时钟脉冲的哪条边沿转换(toggles)输出信号,哪条边沿采样输入信号,还有时钟脉冲的稳定电平值(就是时钟信号无效时是高还是低),详情如下所示:
Mode0:CKP=0,CKE=0 :当空闲态时,SCK处于低电平,数据采样是在第1个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),(发送数据)数据发送是在下降沿。
Mode1:CKP=0,CKE=1 :当空闲态时,SCK处于低电平,数据发送是在第2个边沿,也就是SCK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
Mode2:CKP=1,CKE=0 :当空闲态时,SCK处于高电平,数据采集是在第1个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
Mode3:CKP=1,CKE=1 :当空闲态时,SCK处于高电平,数据发送是在第2个边沿,也就是SCK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
图中黑线为采样数据的时刻,蓝线为SCK时钟信号。
举个例子,下图是SPI Mode0读/写时序,可以看出SCK空闲状态为低电平,主机输出数据在第一个跳变沿被从机采样,主机输入数据同理。
3.STM32相关的SPI
STM32的SPI外设可用作通讯的主机及从机, 支持最高的SCK时钟频率为fpclk/2 (STM32F103型号的芯片默认fpclk1为36MHz, fpclk2为72MHz),完全支持SPI协议的4种模式,数据帧长度可设置为8位或16位, 可设置数据MSB先行或LSB先行。它还支持双线全双工、双线单向以及单线模式。 其中双线单向模式可以同时使用MOSI及MISO数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线, 当然这样速率会受到影响。我们只讲解双线全双工模式。
3.1时钟控制逻辑
时钟由寄存器控制。SCK线的时钟信号,由波特率发生器根据"控制寄存器CR1"中的BR[0:2]位控制,该位是对fpclk时钟的分频因子, 对fpclk的分频结果就是SCK引脚的输出时钟频率,计算方法见表 BR位对fpclk的分频。
其中的fpclk频率是指SPI所在的APB总线频率, APB1为fpclk1,APB2为fpckl2。
通过配置"控制寄存器CR"的"CPOL位"及"CPHA"位可以把SPI设置成前面分析的4种SPI模式。
实际应用中,我们一般不使用STM32 SPI外设的标准NSS信号线,而是更简单地使用普通的GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
3.2通讯过程
STM32使用SPI外设通讯时,在通讯的不同阶段它会对"状态寄存器SR"的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。主发送器通讯过程中的是"主模式"流程,即STM32作为SPI通讯的主机端时的数据收发过程。
3.3从代码层面理解SPI
我们将利用 STM32 的 SPI 来读取外部 SPI FLASH 芯片(W25Q128),实现类似IIC的功能。
使能 SPI2 的时钟
|-------------------------------------------------------------------------------------------------------------------------------------|
| RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PORTB 时钟使能 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE );//SPI2 时钟使能 |
GPIOB端口挂在STM32的APB2时钟线上。SPI2是挂在STM32的APB1时钟线上。
配置相关引脚的复用功能
这里使用 PB13、14、15 这 3 个(SCK.、MISO、MOSI,CS 使用软件管理方式),所以设置这三个为复用 IO。
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化 GPIOB |
SPI2的引脚在PB上,可以参考W25Q128硬件连接图。
初始化 SPI2, 设置 SPI2 工作模式
库函数中是通过 SPI_Init 函数来实现。
|----------------------------------------------------------------------|
| void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct); |
SPI_InitTypeDef 的定义:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| typedef struct { uint16_t SPI_Direction; uint16_t SPI_Mode; uint16_t SPI_DataSize; uint16_t SPI_CPOL; uint16_t SPI_CPHA; uint16_t SPI_NSS; uint16_t SPI_BaudRatePrescaler; uint16_t SPI_FirstBit; uint16_t SPI_CRCPolynomial; }SPI_InitTypeDef; |
SPI_BaudRatePrescaler设置 SPI 波特率预分频值决定 SPI 的时钟的参数,从不分频道 256 分频 8 个可选值
|----------------------------------------|
| SPI_BaudRatePrescaler_256 //256**分频值 |
传输速度为 72M/256=281.25KHz。
初始化代码:
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收 8 位帧结构 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//第二个跳变沿数据被采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由软件控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频 256 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式 SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器 |
使能SPI2
|--------------------------------------------------------------------------------------------|
| SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设 SPI2_ReadWriteByte(0xff); //④启动传输,主机发一个字节,进行一次传输,可以启动传输 |
SPI传输数据
发送数据函数
|-----------------------------------------------------------|
| void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data); |
接收数据函数
|----------------------------------------------------|
| uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx) ; |
查看 SPI 传输状态函数
判断数据是否传输完成,发送区是否为空
判断接收是否完成,接收区是否空
接收
|-------------------------------------------------|
| SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE); |
发送
|-----------------------------------------------|
| SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) |
设置SPI2速度函数
单独的设置分频系数的函数
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| //SPI 速度设置函数 //SpeedSet://SPI_BaudRatePrescaler_256 256 分频 (SPI 281.25K@sys 72M) void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler) { assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler)); SPI2->CR1&=0XFFC7; SPI2->CR1|=SPI_BaudRatePrescaler; //设置 SPI2 速度 SPI_Cmd(SPI2,ENABLE); } |
读写一个字节
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| u8 SPI2_ReadWriteByte(u8 TxData) { u8 retry=0; while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //等待发送区空 { retry++;//重试 if(retry>200)return 0; } //读取两百次还没有值,说明无效,返回 SPI_I2S_SendData(SPI2, TxData); //通过外设 SPIx 发送一个数据 retry=0; while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //等待接收完一个 byte { retry++; if(retry>200)return 0; } return SPI_I2S_ReceiveData(SPI2); //返回通过 SPIx 最近接收的数据 } |
W25Q128
• W25Q128 是华邦公司推出的大容量 SPI FLASH 产品,W25Q128 的容量为 128Mb,该系列还有 W25Q80/16/32/64等。
擦除
W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。
这样要求芯片必须有 4K 以上 SRAM 才能很好的操作。
W25QXX驱动解读
W25QXX.h
W25QXX_CS片选,值0选定,1取消
初始化SPI
读取状态寄存器
写状态寄存器
擦除一个扇区
读取 SPI FLASH
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| //在指定地址开始读取指定长度的数据 //pBuffer:数据存储区 //ReadAddr:开始读取的地址(24bit) //NumByteToRead:要读取的字节数(最大 65535) void W25QXX_Read (u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) { u16 i; SPI_FLASH_CS=0; //使能器件 SPI2_ReadWriteByte(W25X_ReadData); //发送读取命令 SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //发送 24bit 地址 SPI2_ReadWriteByte((u8)((ReadAddr)>>8)); SPI2_ReadWriteByte((u8)ReadAddr); for(i=0;i<NumByteToRead;i++) { pBuffer[i]=SPI2_ReadWriteByte(0XFF); //循环读数 } SPI_FLASH_CS=1; } |
无检查写函数
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| //在指定地址开始写入指定长度的数据,但是要确保地址不越界! //pBuffer:数据存储区 //WriteAddr:开始写入的地址(24bit) //NumByteToWrite:要写入的字节数(最大65535) //CHECK OK void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u16 pageremain; pageremain=256-WriteAddr%256; //单页剩余的字节数 if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节,这也是结束标识 while(1) { W25QXX_Write_Page(pBuffer,WriteAddr,pageremain); if(NumByteToWrite==pageremain)break;//写入结束了 else //NumByteToWrite>pageremain { pBuffer+=pageremain; WriteAddr+=pageremain; NumByteToWrite-=pageremain; //减去已经写入了的字节数 if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节 else pageremain=NumByteToWrite; //不够256个字节了 } //按照页剩余写一次,然后256个字节的写,然后写最后一页多出来的。 }; } |
NoCheck是说可以跨扇区的写
下方表示写了一个扇区
W25QXX_Write函数
作用与 W25QXX_Flash_Read 的作用类似,不过是用来写数据到 W25Q128 里面的,其代码如下:
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| u8 W25QXX_BUFFER[4096]; void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) { u32 secpos; u16 secoff; u16 secremain; u16 i; u8 * W25QXX_BUF; W25QXX_BUF=W25QXX_BUFFER; secpos=WriteAddr/4096;//扇区地址,每个扇区是4096,所以除以4096得到的整数就是扇区的地址标号 secoff=WriteAddr%4096;//在扇区内的偏移 secremain=4096-secoff;//扇区剩余空间大小 //printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用 if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于 4096 个字节 while(1) { W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容 //secpos*4096是该扇区的起始地址 for(i=0;i<secremain;i++)//校验数据 { if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除,偏移地址内有数据 //擦除后的默认值是0xFFF } if(i<secremain)//需要擦除 { W25QXX_Erase_Sector(secpos); //擦除这个扇区 for(i=0;i<secremain;i++) //复制 { W25QXX_BUF[i+secoff]=pBuffer[i]; } W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区 } else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain); //如果扇区剩余空间足够,直接写入扇区剩余区间. //是否需要写入下一个扇区 if(NumByteToWrite==secremain)break;//写入结束了 else//写入未结束 { secpos++;//扇区地址增 1 secoff=0;//偏移位置为 0 pBuffer+=secremain; //指针偏移 WriteAddr+=secremain; //写地址偏移 NumByteToWrite-=secremain; //字节数递减 if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完 else secremain=NumByteToWrite; //下一个扇区可以写完了 } }; } //跟无检查页写入的逻辑一致。 |
该函数可以在 W25Q128 的任意地址开始写入任意长度(必须不超过 W25Q128 的容量)的数据。
先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。
擦除的最小单位是扇区,也就是4K。所以在擦除之前我们先将这个扇区的数据读取出来,保存在缓存区。在缓存中将对应的地址更新之后,一次性将数据写到对应的sector之中。
当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。
我理解,整个STM32读写W25Q128是不断封装SPI_I2S_SendData和ReadData的一个过程,先封装到读写一个字节ReadWriteByte,再封装到读写一个扇区W25QXX_Write_NoCheck,W25QXX_Write。整个过程比较标准,无需太多改动。
4.STM32之SPI实战
在实现STM32的SPI通讯之前,先做个小实验,实现USART串口通讯。因为调试STM32的SPI通讯可以把结果通过串口打印到电脑上显示,方便观察结果。
STM32CubeMX学习笔记(6)------USART串口使用_unused(huart)-CSDN博客
5.Keil调试代码
今天学了Keil的debug功能,刚开始程序卡在了HAL_INIT这里,这是个很奇怪的问题。CubeMx生成的代码段,什么都没有做就是无法调试。原来要在CubeMx里面勾选一个调试功能。
在KEIL中勾选Use-MicroLib库
因为调用了printf,这是一个C++里面的功能,需要重映射。代码如下。
新建一个retarget.c文件。
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #include "stdio.h" #include "stm32f1xx_hal.h" #include "usart.h" #pragma import(__use_no_semihosting_swi) #pragma import(__use_no_semihosting) void _sys_exit(int x) { x = x; } struct __FILE { int handle; /* Whatever you require here. If the only file you are using is */ /* standard output using printf() for debugging, no file handling */ /* is required. */ }; /* FILE is typedef' d in stdio.h. */ FILE __stdout; void _ttywrch(int ch){}; int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff); return ch; } |
这段代码也是调了好久,网站上的或多或少不太对。
有了上面的代码,main函数就可以调用printf函数,打印到串口显示出来。
Stm32 debug停留在"BKPT 0xAB"或者"SWI 0xAB"的解决办法。
通过百度网盘分享的文件:SPI
链接:https://pan.baidu.com/s/1qzSFFV8-Vhrb0NzqbBAeCg?pwd=sshc
提取码:sshc