引子

最近跟着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){
//1.初始化引脚
//重映射

GPIO_InitTypeDef gpio_structure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SPI1,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);

//PB3 SCK AF_PP 2MHz
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);

//PB4 MISO IPU 2MHz
gpio_structure.GPIO_Mode=GPIO_Mode_IPU;
gpio_structure.GPIO_Pin=GPIO_Pin_4;
GPIO_Init(GPIOB,&gpio_structure);

//PB5 MOSI PP 2MHz
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);

//PA15 NSS PP 2MHz
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);

//2. SPI本身进行初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);


SPI_InitTypeDef spi_initStruct;
spi_initStruct.SPI_Mode=SPI_Mode_Master;//主机模式
spi_initStruct.SPI_DataSize=SPI_DataSize_8b;
//mode 0 根据W25Q16芯片手册
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){
//1.闭合总开关
SPI_Cmd(SPIx,ENABLE);
//2.写入第一个字节
SPI_I2S_SendData(SPIx,pDataTx[0]);
//3.
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);

}
//4.读出最后的一个字节
while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_RXNE)==RESET);
pDataRx[Size-1]=SPI_I2S_ReceiveData(SPIx);


//5.断开总开关
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];

//1.写使能 向主机发0x06 查询手册
buffer[0]=0x06;
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);//通过SPI通信
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中 NSS=1


//2.扇区擦除
//主机发0x20+24位地址
buffer[0]=0x20;
buffer[1]=0x00;
buffer[2]=0x00;
buffer[3]=0x00;

GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,4);//通过SPI通信
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中 NSS=1

//3.等待空闲 查询busy标志位
while(1){
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
buffer[0]=0x05;
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);//写0x05
buffer[0]=0xff;
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);//读状态寄存器1的当前值 buffer0
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中 NSS=1

//busy标志位是第一位 从1变成0 就退出
if((buffer[0]&0x01)==0) break;
}
//4.写使能
buffer[0]=0x06;
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);//通过SPI通信
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中 NSS=1

//5.页编程
buffer[0]=0x02;
buffer[1]=0x00;
buffer[2]=0x00;
buffer[3]=0x00;
buffer[4]=Byte;

GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,5);//发送5个字节
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中 NSS=1


//6.等待空闲
while(1){
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
buffer[0]=0x05;
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);//写0x05
buffer[0]=0xff;
Myy_SPI_MasterTransmitReceive(SPI1,buffer,buffer,1);//读状态寄存器1的当前值 buffer0
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中 NSS=1

//busy标志位是第一位
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];
//发送0x03+24位地址,然后读取数据
buffer[0]=0x03;
buffer[1]=0x00;
buffer[2]=0x00;
buffer[3]=0x00;

GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中 NSS=0
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);//取消选中 NSS=1
c
return buffer[0];

}//把保存的字节读出来


网络比较慢的家人图片可能加载不出来,可以上个“魔法”试试

SPI基本原理

电路结构

image-20250308135405908

一主多从

MOSI

MISO

SCK

NSS(低电压被选中)

通信流程

image-20250308135543726

SPI发送数据和接收数据时同时

时钟信号的极性

image-20250308135728649

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

image-20250308135912783

相位

image-20250308140055333

image-20250308160944828

4种时钟模式

image-20250308161017753

比特位传输顺序

LSB MSB

image-20250308161046611

数据宽度
image-20250308161122041

SPI 端口配置

IO引脚的输入输出模式

image-20250310215611245

image-20250310215647448

MISO

MOSI

SCK

NSS

SPI模式配置

主要的电路

image-20250315210055911

SPI通信方向

image-20250315210800145

关于spi的参数

要看具体的flash模块的说明书 instruction部分

如:

image-20250315212238282

spi模块 NSS信号线

image-20250315212422627

作为从机:接入高电压

硬件NSS外部配置 直接拉到3.3

也可以软件NSS 写1

SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Set);

SPI数据收发

数据收发的特点

双向的同时的,每发送一个bit必然接受一个bit

image-20250315212757809

数据收发原理

image-20250315213301416

具体的编程

image-20250315213625345

W25Q64实验

注意,我这边实际应用到的是W25Q16模块

W25Q64内部结构

image-20250316092830017

使用模块写数据

扇区擦除 页编程

image-20250316092900333

写使能

image-20250316113355377

扇区擦除

image-20250316113552252

等待空闲

等待扇区擦除指令完成

image-20250316113757183

页编程

image-20250316113930442

使用模块读数据

image-20250316114156038

小应用

按键+串口

先铺垫一下按键和串口 也发到了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);
}//读取LED灯所在引脚的输出状态

else{
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
Myy_W25Q16_SaveByte(0x02);
}
}
}
Delay(10);
}