引子
最近跟着b站铁头山羊在学stm32单片机,目前学到SPI通信,主要就是想记录一下整个复现代码的过程,然后后面可以积累一些经验。这个up讲的很通俗,推荐大家去看看。
铁头山羊 stm32学习
首先刚开始我跟着他敲是完全没有完成的,明白一点说就是代码复现不了,啥都没有。然后只能回看回看,再回看。
直到晚上,我才发现了所有的bug。。。。
其实80%的问题都是自己的代码配置的问题,先怀疑一下自己的代码,然后在怀疑硬件本身的问题吧~
关于bug 的解决
我这边bug主要在哪个地方呢?
在没有解决这些bug之前,我的代码时卡死在读RXNE标志位,是否变成了非空,一直在那循环。我就觉得不对劲,后来发现:
1⃣
第一个就是引脚配置,PB5 PB3 虽然他俩都是AF_PP输出的模式,我真的就直接复制了,居然没有改引脚名!
2⃣
第二个,没有初始化SPI的时钟,多么离谱。只是因为up在讲解的时候没有特地讲,但是他的代码实际上是配置了的,所以我就忽略了,太离谱了。
3⃣
第三个,在写spi通信的过程的buffer,明明应该是buffer[0]=0x03,buffer[1]=0x00,这种形式,我居然写成了索引全是buffer[0]。
以上bug发现完了我以为结束了,其实没有。
然后 代码情况是,不管我发什么,接收回来的都是0。
经过挣扎,我再去看了一遍代码,仔仔细细。结果发现:
原来是我用的端口PA15,他是默认JTAG的端口,要给他失能,虽然当时up提了,但是我还是水灵灵地给他最后disable,虽然前面一个参数带了disable。
最后终于可以了!!!
回顾一下spi通信的过程!
首先总结一下,无论是什么通信,都一般有几部分
1.时钟开启
2.端口初始化(这个功能要用到哪些端口
3.功能配置(比如spi功能的配置 你需要了解一下这些功能的参数表示什么 有什么用
4.根据上述配置好的,进行逻辑功能的编写
主要代码
主要功能 :向flash里面写一个数据,然后再从这个flash里面把数据读出来,整个过程通过串口进行显示
主要的代码放在下面了,亲测有效
main.c
1 2 3 4 5 6 7
| uint8_t a=4; Usart_Init(); MY_SPI_Init(); My_USART_Printf(USART1,"%d\r\n",a); Myy_W25Q16_SaveByte(0x09); a=Myy_W25Q16_LoadByte(); My_USART_Printf(USART1,"%d\r\n",a);
|
void MY_SPI_Init(void)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| void MY_SPI_Init(void){ GPIO_InitTypeDef gpio_structure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SPI1,ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); gpio_structure.GPIO_Mode=GPIO_Mode_AF_PP; gpio_structure.GPIO_Pin=GPIO_Pin_3; gpio_structure.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOB,&gpio_structure); gpio_structure.GPIO_Mode=GPIO_Mode_IPU; gpio_structure.GPIO_Pin=GPIO_Pin_4; GPIO_Init(GPIOB,&gpio_structure); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); gpio_structure.GPIO_Mode=GPIO_Mode_AF_PP; gpio_structure.GPIO_Pin=GPIO_Pin_5; gpio_structure.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOB,&gpio_structure); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); gpio_structure.GPIO_Mode=GPIO_Mode_Out_PP; gpio_structure.GPIO_Pin=GPIO_Pin_15; gpio_structure.GPIO_Speed=GPIO_Speed_2MHz; GPIO_Init(GPIOA,&gpio_structure); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); SPI_InitTypeDef spi_initStruct; spi_initStruct.SPI_Mode=SPI_Mode_Master; spi_initStruct.SPI_DataSize=SPI_DataSize_8b; spi_initStruct.SPI_CPOL=SPI_CPOL_Low; spi_initStruct.SPI_CPHA=SPI_CPHA_1Edge; spi_initStruct.SPI_FirstBit=SPI_FirstBit_MSB; spi_initStruct.SPI_Direction=SPI_Direction_2Lines_FullDuplex; spi_initStruct.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_256; spi_initStruct.SPI_NSS=SPI_NSS_Soft; SPI_Init(SPI1,&spi_initStruct); SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Set);
}
|
void Myy_SPI_MasterTransmitReceive(SPI_TypeDef *SPIx, uint8_t *pDataTx,uint8_t *pDataRx,uint16_t Size)
这一块就是spi数据数据发送和接收的过程,要注意的是spi通信这边设置的是全双工的通信,所以每次发送一个数据就会对应接收到一个数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void Myy_SPI_MasterTransmitReceive(SPI_TypeDef *SPIx, uint8_t *pDataTx,uint8_t *pDataRx,uint16_t Size){ SPI_Cmd(SPIx,ENABLE); SPI_I2S_SendData(SPIx,pDataTx[0]); for(uint16_t i=0;i<Size-1;i++){ while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_TXE)==RESET); SPI_I2S_SendData(SPIx,pDataTx[i+1]); while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_RXNE)==RESET); pDataRx[i]=SPI_I2S_ReceiveData(SPIx); } while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_RXNE)==RESET); pDataRx[Size-1]=SPI_I2S_ReceiveData(SPIx); SPI_Cmd(SPIx,DISABLE); }
|
void Myy_W25Q16_SaveByte(uint8_t Byte)
这一块比较重要 函数是向W25Q16去写数据
主要是和spi通信的过程
通俗总结一下就是 发一个指令(这个指令要在对应从设备的手册去找,让主机给它发送指令,建立通信关系)+ 这个指令对应的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| void Myy_W25Q16_SaveByte(uint8_t Byte){ uint8_t buffer[10]; buffer[0]=0x06; GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET); buffer[0]=0x20; buffer[1]=0x00; buffer[2]=0x00; buffer[3]=0x00; GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,4); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);
while(1){ GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); buffer[0]=0x05; Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1); buffer[0]=0xff; Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET); if((buffer[0]&0x01)==0) break; } buffer[0]=0x06; GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET); buffer[0]=0x02; buffer[1]=0x00; buffer[2]=0x00; buffer[3]=0x00; buffer[4]=Byte;
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,5); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET); while(1){ GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); buffer[0]=0x05; Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1); buffer[0]=0xff; Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1); GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET); if((buffer[0]&0x01)==0) break; }
}
|
uint8_t Myy_W25Q16_LoadByte(void)
把对应的地址上的数据读出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| uint8_t Myy_W25Q16_LoadByte(void){ uint8_t buffer[10]; buffer[0]=0x03; buffer[1]=0x00; buffer[2]=0x00; buffer[3]=0x00; GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET); Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,4); buffer[0]=0xff; Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET); c return buffer[0]; }
|
网络比较慢的家人图片可能加载不出来,可以上个“魔法”试试
SPI基本原理
电路结构

一主多从
MOSI
MISO
SCK
NSS(低电压被选中)
通信流程

SPI发送数据和接收数据时同时
时钟信号的极性

在空闲状态下,SCK上是低电压,就是低级性;如果SCK是高电压,就是高极性

相位


4种时钟模式

比特位传输顺序
LSB MSB

数据宽度
SPI 端口配置
IO引脚的输入输出模式


MISO
MOSI
SCK
NSS
SPI模式配置
主要的电路

SPI通信方向

关于spi的参数
要看具体的flash模块的说明书 instruction部分
如:

spi模块 NSS信号线
作为从机:接入高电压
硬件NSS外部配置 直接拉到3.3
也可以软件NSS 写1
SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Set);
SPI数据收发
数据收发的特点
双向的同时的,每发送一个bit必然接受一个bit

数据收发原理

具体的编程

W25Q64实验
注意,我这边实际应用到的是W25Q16模块
W25Q64内部结构

使用模块写数据
扇区擦除 页编程

写使能

扇区擦除

等待空闲
等待扇区擦除指令完成

页编程

使用模块读数据

小应用
按键+串口
先铺垫一下按键和串口 也发到了csdn上
主要功能是:按键按一下,串口输出的数字+1,双击则清零,长按则持续加1
按键+串口
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| int main(void) { LED_Key_Init(); Usart_Init(); Button_Init(); int click=0; while(1) { click=Get_Cilcks(); Key_Usart_2(click); } }
|
int Get_Cilcks(void)函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| int Get_Cilcks(void){ uint16_t cur=SET,pre=SET; cur=Key_Status; int click=0; uint32_t now = GetTick(); int time=300; while(GetTick()-now<time){ cur=Key_Status; if(cur!=pre){ if(Key_Status==RESET){ Delay(50); if(Key_Status==RESET){ now=GetTick(); click++; } } if(click>3){ cnt++; My_USART_Printf(USART1,"%d\r\n",cnt); } } pre=cur; } return click; }
|
void Key_Usart_2(int click)函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void Key_Usart_2(int click){ volatile uint32_t currentTime = GetTick(); int cur=Key_Status; switch(click){ case 1: cnt++; My_USART_Printf(USART1,"%d\r\n",cnt); break; case 2: cnt=0; My_USART_Printf(USART1,"%d\r\n",cnt); break; default: break; } }
|
按键+LED+W25Q16
记录上一次LED的亮灭情况 然后读写flash 掉电不丢失
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| LED_Key_Init(); MY_SPI_Init(); int pre_status=Bit_SET,cur_status=Bit_SET; uint8_t a=Myy_W25Q16_LoadByte(); if(a==0x12) GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET); else GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
while(1){ pre_status=cur_status; cur_status=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1); if(pre_status!=cur_status){ if(cur_status==Bit_SET){ if(GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13)==Bit_RESET){ GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET); Myy_W25Q16_SaveByte(0x12); } else{ GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET); Myy_W25Q16_SaveByte(0x02); } } } Delay(10); }
|