这一篇的图基本都来自si24r1芯片的数据手册
另外一些BUG比如发送没问题,接收全是0xff;你可以上点助焊剂,把nf03用热风枪吹吹;我不知道哪出的问题但我是这么解决的。

SPI总线

SPI相对于uart,i2c的速度快很多,很多小屏幕都用SPI的;是同步高速全双工通讯
F1的SPI时钟最高可以到18Mhz,支持DMA
说一下信号都是什么
SCL:时钟
MOSI:主机输出从机输入(主机—>从机)
MISO:主机输入从机输出(从机—>主机)
NF_CSN(自己找引脚设置):片选信号(低电平有效),每个从机都要有一个片选信号,让从机知道是自己接收
上面是SPI的数据线
我们还有几根线
NF_CE:使能
NF_IRQ:中断引脚(从机发送给主机)低电平有效

基本知识

SPI有四种采集数据的时序方式(时钟空闲时是高是低,上升还是下降沿采集)
看上面括号大概就能知道:时钟空闲高上升沿采集,时钟空闲高下降沿采集,时钟空闲低上升沿采集,时钟空闲低上升沿采集
通过SPI_CR寄存器的CPOL和CPHA位控制,CPOL控制空闲时时钟是高(1)是低(0),CPHA控制时钟开始时第一个(0)还是第二个(1)边沿采集
image-1678250608602
具体怎么采集,还要看从设备

全双工的SPI的读写是同时操作的如果只写的话,那么就是忽略接收的字节

SPI初始化【介绍】(除了选全双工主设备,都默认的)

stm32cubeMX的SPI2使能一下就行,主机全双工
关于NSS配不配置,我们自己控制片选引脚,所以就直接disable。
两种SPI,motorloa模式和TI模式,我们一般用的都是motorloa的,另外F1这里没得选,直接motorloa
然后每次传输多少位,我们就8位
先写低位还是先写高位
image-1678272521219
我们先写高位,选MSB
下面一行预分频系数prescaler应该是2,这个不改,改动他的话是控制下面的18M的比特率,F1的SPI最大为fPCLK/2,也就是18M
image-1678277094737
看图,空闲时CLK是低电平,数据在第一个时钟沿变化时采集到,另外,这个图前面C7,D7也能看出来是先写高位
也就是CPOL=0,CPHA=0,在cubeMX上选择就是CPOL=low,CPHA=1Edge
然后循环冗余校验(CRC),就不开了,不会用【用这一套代码,开CRC你会发现波形乱了】

cubeMX的选项

先选SPI的,也就是MOSI,MISO,SCK。
打开全双工就行
image-1678791885930

使能,片选引脚
就正常的推挽输出,另外改一下名字(user label),我的代码都是按这名字写的
image-1678792052506
中断引脚
使能引脚EXTI,下降沿触发
image-1678798539487
开启NVIC
image-1678798586974

发送接收函数

HAL库的SPI发送接收函数需要的参数是数组,很麻烦,我们可以自己写一个
用到两个寄存器
image-1678413347391
状态寄存器里面的发送接收状态
比如发送,等到发送缓冲位空的时候再发送(没有数据再写)
接收则到接收缓冲不为空的时候再接收(有数据再读),读完以后,RXNE会自动清零
image-1678413818357
这个DR寄存器,对应发送和接收缓冲,具体怎么分的我不清楚
image-1678417596554
我分成两种写(标准库,HAL库),虽然看着的都是用HAL库;
因为我一开始按标准库的写,结果发现标准库没有一点问题的代码,在HAL库里面操作以后,寄存器不对劲,比如我传数据,给DR寄存器写完后,发送缓冲标志TXE从1变0,然后再也不变回1;接收数据,RXNE一直都是0,就没变过1。很没办法,HAL库我只好用他给的函数了。

标准库

下面是代码,逻辑是等待发送缓冲为空(TXE=1)【不为空也就是TXE=0的时候,一直在循环里,TXE=1跳出循环】就发送,时间长了还不为空就报错
然后等接收缓冲区不为空(RXNE=1)【为空(RXNE=0)的时候,一直在循环里,RXNE=1以后跳出】后接收数据。
发送数据作为参数;接收的数据作为返回值

uint8_t SPI2_ReadWriteByte(uint8_t TxData)
{
	uint8_t count=0;
	while((SPI2->SR&1<<1)==0){
		count++;
	if(count>=0xFF)return 0;
	}
	SPI2->DR=TxData;
	
	count=0;
	while((SPI2->SR&1<<0)==0){
		count++;
	if(count>=0xFF)return 0;
	}
	return SPI2->DR;
}

但是我遇到了一个问题(HAL库用这个函数),发送时,给DR数据,然后SR寄存器里面发送缓冲标志一直显示发送缓冲里有数据(数据发不出去,一直在寄存器里)
接收更不用说,没数据来过,接收缓冲标准一直是显示数据是空的
696405d200ba27f7f33b86f24eaa58b

HAL库

HAL库有这个函数HAL_SPI_TransmitReceive;直接实现上面的操作,我这么写只是因为我后面代码都写了,这样好移植。

 uint8_t SPI2_ReadWriteByte(uint8_t TxData)
{
    uint8_t Rxdata;
    HAL_SPI_TransmitReceive(&hspi2,&TxData,&Rxdata,1, 1000);       
    return Rxdata;    
}

NF-03(si24r1)

si24r1有一组状态机
image-1678352598066
这个图从小马哥的视频里偷出来的,比较直观的看出几个状态怎么控制
image-1678352483391

数据包

si24r1基于包通讯,各种东西都是硬件实现的,我们使用只要配置一点东西
image-1678360862620
前导码不管
地址,比如1号给2号发信息,1号NF-03发送地址0x03,那么2号NF-03接收地址就也是0x03
包控制字:
数据包长度:这,看图吧,6位的长度,设置长度只能0到32字节
PID子段:发射方通过 SPI 写 FIFO,PID 的值自动累加。
NO-ACK:设置1就接收后不返回信号,设置0就返回信号,我们设置0

接收工作流程:收到数据,去掉前导码,用地址拿到FIFO里的数据,产生中断信号

关于负载长度,也就是数据包长度有动态的也有静态的,动态的要去配置 FEATURE 寄存器中的 EN_DPL 位与 DYNPD 寄存器中的DPL_P0 位
image-1678367185188
接收端有6个接收通道,可以接6个设备的
image-1678367163503
看上面的图,接收方的6个通道的地址,P0是可以随便写的,P1-P5是高位共用的,最后8位是自己随便写。

image-1678277094737
SPI的时序图可以看到先发的是命令数据,下面的表是各种命令都是什么
image-1678369342490

一些基本代码

在初始化以前,我们先写两个函数用来操作芯片的寄存器;因为HAL库只有写一串数组的命令,这样写单个字符很麻烦,不嫌麻烦可以不写
关于读写寄存器,我们直接在代码里面让输入的寄存器加上0x00或0x20,从而不用让我们自己算数。

操作CE和CSN引脚高低的宏

#define NF_CSN_LOW	GPIOB->BSRR|=NF_CSN_Pin<<16
#define NF_CSN_HIGH	GPIOB->BSRR|=NF_CSN_Pin
#define NF_CE_LOW	GPIOA->BSRR|=NF_CE_Pin<<16
#define NF_CE_HIGH	GPIOA->BSRR|=NF_CE_Pin

读寄存器

上面表里的命令,读寄存器 R_REGISTER命令是000A AAAA
那么只要给设备发送数据0x00+寄存器地址就行,但是数据是命令发完以后才给发过来的,所以下面还要一组读命令,才能读到
所以操作是1.片选,2.发读的寄存器的命令,3.读传过来的数据,4.关片选

uint8_t NF03_ReadReg(uint8_t reg)
{
	uint8_t reg_val;
	reg=0x00+reg;
	NF_CSN_LOW;
	SPI2_ReadWriteByte(reg);
	reg_val = SPI2_ReadWriteByte(0xff);
	NF_CSN_HIGH;
	return reg_val;
}

为什么第二次读的时候往里面写0xff,这是我抄的,我不太清楚大概说下,因为读命令,往里写什么都一样,就随便写的;不要看上面NOP命令是1111 1111就以为是发的NOP命令,上面时序读命令后面是空的。即使是写命令那个时序,命令发完后发送的也都是数据了,而不是命令。

读很多次

上面我们有读一次的代码,然后循环几次就是读很多次。

uint8_t NF03_ReadBuf(uint8_t reg,uint8_t *value,uint8_t len)
{
	uint8_t status,i;
	reg=0x00+reg;
	NF_CSN_LOW;
	status=SPI2_ReadWriteByte(reg);
	for(i=0;i<len;i++){
		*value = SPI2_ReadWriteByte(0xff);
		value++;
	}
	NF_CSN_HIGH;
	return status;
}

写寄存器

上面表里的命令,写寄存器 W_REGISTER命令是001A AAAA
那么只要给设备发送数据0x20+寄存器地址就行
我们只发送了命令,但是写什么东西还不知道
但是看上面的SPI时序,片选后先发完命令,然后发数据,数据发完了就关了片选
所以操作是:1.片选,2.写哪个寄存器的命令(0x20+寄存器地址),3.写的数据,4.关片选

uint8_t NF03_WriteReg(uint8_t reg,uint8_t value)
{
	uint8_t status;
	reg=0x20+reg;
	NF_CSN_LOW;
	status=SPI2_ReadWriteByte(reg);
	SPI2_ReadWriteByte(value);
	NF_CSN_HIGH;
	return status;
}

写很多次

上面我们有写一次的函数,那么循环也就可以写很多次

uint8_t NF03_WriteBuf(uint8_t reg,uint8_t *value,uint8_t len)
{
	uint8_t status,i;
	reg=0x20+reg;
	NF_CSN_LOW;
	status=SPI2_ReadWriteByte(reg);
	for(i=0;i<len;i++){
		SPI2_ReadWriteByte(value[i]);
	}
//	HAL_SPI_Transmit(&hspi2,value,len,200);
	NF_CSN_HIGH;
	return status;
}

设置模式(发送接收)

image-1678441282554
image-1678352483391
看表,发送模式就是配置0x0E(0000 1110),接收模式就是0x0F(00001111)
不过配置发送接收也就是最后一位,前面设置是我们用这里的CRC和中断
关断模式,待机模式我们都不考虑,都开机了哪有这两个模式的事
另外,因为有中断,切换模式的时候记得吧中断标志位去掉,下面寄存器0x7E(0111 1110)
image-1678441475788
只有两个状态要设置,所以参数是0就发送,不是0就接收模式

void NF03_SetMode(uint8_t mode)
{
	if(mode==0)
	{
		NF_CE_LOW;
		NF03_WriteReg(0x00,0x0E);//设置发送
		NF03_WriteReg(0x07,0x7E);//清除所有标志位
		NF_CE_HIGH;
	}
	else
	{
		NF_CE_LOW;
		NF03_WriteReg(0x00,0x0F);//设置接收
		NF03_WriteReg(0x07,0x7E);//清除所有标志位
		NF_CE_HIGH;
	}
}

初始化

写寄存器要在Shutdown、Standby、Idle-TX模式下,所以在初始化的时候,CE引脚要拉低,进入待机模式

步骤:
设置地址长度
设置TX节点地址
设置RX节点地址
设置自动重发次数
使能PIPE0
使能ACK
设置通道0的数据宽度
设置通讯频率
设置发射参数(低噪放大器增益,发射功率,无线速率)
配置si24r1工作模式

设置地址长度

image-1678437733309
就用5位的地址

设置TX节点地址

image-1678437864802
因为要配置ACK信号,所以在设置RX_ADDR_P0也就是RX接收地址的时候,要让两个地址相同
注意因为这里是低位先写入的,所以地址倒过来写。
比如

设置RX节点地址

上面设置TX说了,地址要相同
image-1678437910785

设置自动重发次数

image-1678438058209
随便设置,我这里是0x1A(0001 1010);重发延时是500u,重发次数是10次

使能PIPE0

image-1678438207226

使能ACK

image-1678438244419

设置通道0的数据宽度

image-1678439213309
我们用固定的数据长度32;也就是后面发送数据一定都是32位的

设置通讯频率

image-1678439431509
随便设置不过好像两个通讯的芯片要在一个信道

设置发射参数(低噪放大器增益,发射功率,无线速率)

image-1678439503283
设置大的发射功率,小的数据率,不动恒载波发射模式不用
设置0x27(0010 0111)

配置si24r1工作模式

默认状态就让他接收,有发数据的需求的时候在设置发送

总体

uint8_t TX_ADDRESS[]={0xA1,0xB2,0xC3,0xD4,0xE5};//地址,随便设置
void NF03_Config(void)
{
	NF_CSN_LOW;
	NF_CSN_HIGH;//这两个是方便逻辑分析仪测试的,不加的话前面几个时钟会识别错误(不这样设置一下,刚复位的时候的电平会被分析进去,然后测不出来东西,没用逻辑分析仪可以不看,用过的都懂)
	
	NF_CE_LOW;
	NF03_WriteReg(0x03,0x03);//设置通信地址长度,默认里面是0x03,不过还是设置下,也就是长度5
	NF03_WriteBuf(0x10,(uint8_t*)TX_ADDRESS,5); //TX地址,长度是5
	NF03_WriteBuf(0x0A,(uint8_t*)TX_ADDRESS,5); //RX地址,因为要收ACK所以RX地址与TX地址要一样
	NF03_WriteReg(0x04,0x1A); //设置重发,看心情设置
	
	NF03_WriteReg(0x02,0x01);//使能通道0 
	NF03_WriteReg(0x01,0x01); //通道0自动应答(ack)
	NF03_WriteReg(0x11,0x20);//设置通道0宽度32(0010 0000)
	NF03_WriteReg(0x05,0x32); //设置RF通道,也就是频率,随意设置
	NF03_WriteReg(0x06,0x27); //设置发射参数
	
	NF03_SetMode(1); //默认接收模式
	NF_CE_HIGH;
}

到这里初始化就结束了,可以先去测试一下,读一个0x10寄存器,用串口打印出来看看。

发送与接收数据

上面基本操作已经弄好了,后面利用状态位产生中断,来控制两个NF-03通讯(发送接收)
没用特殊操作就让模块一直在接收模式
当需要发数据的时候,再变成发送模式。
状态转换的函数上面已经写过了,我就是在这重复说一下
状态的转换我们再中断里面写,收发函数我们只实现接收和发送功能
虽然这么说,但是还要另外清除中断,不然会出事。
image-1678352598066
image-1678369342490
看表,读取rxfifo是往0x61(0110 0001)发送信号,发射txFIFO是往0xA0发送数据
清空rxfifo是0xE2,清空txfifo是0xe1,另外rxfifo要在ACK操作完成后再清空(不过我们不用管,自动发)

接收数据

接收我们就从RX_FIFO里面读出来数据,然后清除FIFO

uint8_t NF03_RxPacket(uint8_t *rxbuf)	//接收数据
{
	NF_CE_LOW;
	NF03_ReadBuf(0x61,rxbuf,32);
	NF03_WriteReg(0xE2-0x20,0xff);
	NF_CE_HIGH;
}

fifo里面数据长度是32没问题吧,上面我们初始化的时候设置的32的长度
至于上面0xE2-0x20这么写地址,是因为上面我们写writereg的时候,有写过让reg+0x20的代码,这里我们就是让SPI发一个0xE2的数据,所以再减去0x20;
如果不这么写,再写一个SPI发送数据的代码也行。

发送数据

void nRF24L01P_TxPacket(uint8_t *txbuf)//发送数据
{
	NF_CE_LOW;	
	NF03_WriteBuf(0xA0-0x20,txbuf,32); //向txfifo发数据
	NF03_WriteReg(0x00,0x0E);//设置发送
	NF03_WriteReg(0x07,0x7E);//清除所有标志位
	NF_CE_HIGH;
	delay_us(10);	//让CE置高一会,保证数据发出去
}

上面的延时函数

还是用nop实现的,还得是nop方便
在做全彩灯那里说过,我们72M的时钟,一nop()就是13.8纳秒,一毫秒大概就是72个nop,所以算一下大概就出来了
(我没测试,如果不是的话,大概这个循环也占用时间了,自己改一下)

void delay_us(uint8_t time)
{
	uint16_t count;
	count = time*72;
	do
	{
		__NOP();
	}while(count--);
}

外部中断

image-1678804192963
。。这个图片我放的就很SB,那就当补课吧,中断可以有什么函数可以来这里看
image-1678806045960
这个函数里面自带的函数HAL_GPIO_EXTI_IRQHandler会清空中断标志位,然后进入我们可以自己编写的HAL_GPIO_EXTI_Callback函数;IIC那里我们说过这个了,这里再提一句。
我们可以在it.c里面找到这个函数,在里面写
不过我个人喜欢把他删了,去自己文件下写,所以我剪切到nf03.c里面

这里提一个函数__HAL_GPIO_EXTI_GET_IT
image-1678807818513

__HAL_GPIO_EXTI_GET_IT() 检查某个外部中断线是否有挂起(Pending)的中断
__HAL_GPIO_EXTI_CLEAR_IT() 清除某个外部中断先的挂起标志位
__HAL_GPIO_EXTI_GENERATE_SWIT() 在某个外部中断线上产生软中断
__HAL_GPIO_EXTI_GET_FLAG(EXTI_LINE)它的功能就是检查外部中断挂起寄存器(EXTI_PR)中某个中断线的挂起标志位知否置位
__HAL_GPIO_EXTI_GENERATE_SWIT(EXTI_LINE)在某个外部中断线上产生软中断
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) 外部中断ISR函数中调用的通用处理函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) 外部中断处理的回调函数,需要用户重新实现

不太清楚为什么加这个,但大家都用他再中断函数里先判断一下是否还在中断
然后看着表我们往下写
image-1678441475788
先判断是不是TX_DS 0x20(0010 0000),也就是是不是发射完成产生的中断,如果是的话,把芯片的状态变成接收模式;然后清除发送完成的标志(写1),清除发送的TXFIFO(命令里面的FLUSH_TX:0xE1)
再判断是不是RX_DS 0X40(0100 0000),也就是是不是RXFIFO有东西接收了,如果是的话,就把数据读取出来【另外这里后面接收遥控的数据,应该直接去把PWM波修改掉,我们还没做PWM,先空着,后面我再加】;然后清除发送完成的标志
再判断MAX_RT 0x10(0001 0000),是不是重发次数到最大了还没发出去。反正也发不出去了,就换成接收状态完事。然后清除标志并清除TXFIFO。
另外两个只能读,而且感觉没啥用了。

void EXTI2_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI2_IRQn 0 */
	uint8_t sta;
	if(__HAL_GPIO_EXTI_GET_IT(NF_IRQ_Pin) !=0)
	{
		NF_CE_LOW;	//CE置低以便读写数据
		sta = NF03_ReadReg(0x07);	//读取status里面的数据,以确定是什么触发的中断
		if(sta&0x20)
		{
			NF03_SetMode(1);	//设置接收模式
			NF03_WriteReg(0x07,0x20);	//给TX_DS写1,清除发送完成标志位
			NF03_WriteReg(0xE1-0x20,0xFF);	//清除TXFIFO
		}
		if(sta&0x40)
		{
			NF03_RxPacket(NF_RX_DATA);
			
			NF03_WriteReg(0x07,0x40);	//给RX_DS写1,清除接收完成标志位
		}
		if(sta&0x10)
		{
			NF03_SetMode(1);	//设置接收模式
			NF03_WriteReg(0x07,0x10);	//给MAX_RT写1,清除最大重发次数标志位
			NF03_WriteReg(0xE1-0x20,0xFF);	//清除TXFIFO
		}
		
	}
	
  /* USER CODE END EXTI2_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(NF_IRQ_Pin);
  /* USER CODE BEGIN EXTI2_IRQn 1 */

  /* USER CODE END EXTI2_IRQn 1 */
}