这一篇的图基本都来自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)边沿采集
具体怎么采集,还要看从设备
全双工的SPI的读写是同时操作的如果只写的话,那么就是忽略接收的字节
SPI初始化【介绍】(除了选全双工主设备,都默认的)
stm32cubeMX的SPI2使能一下就行,主机全双工
关于NSS配不配置,我们自己控制片选引脚,所以就直接disable。
两种SPI,motorloa模式和TI模式,我们一般用的都是motorloa的,另外F1这里没得选,直接motorloa
然后每次传输多少位,我们就8位
先写低位还是先写高位
我们先写高位,选MSB
下面一行预分频系数prescaler应该是2,这个不改,改动他的话是控制下面的18M的比特率,F1的SPI最大为fPCLK/2,也就是18M
看图,空闲时CLK是低电平,数据在第一个时钟沿变化时采集到,另外,这个图前面C7,D7也能看出来是先写高位
也就是CPOL=0,CPHA=0,在cubeMX上选择就是CPOL=low,CPHA=1Edge
然后循环冗余校验(CRC),就不开了,不会用【用这一套代码,开CRC你会发现波形乱了】
cubeMX的选项
先选SPI的,也就是MOSI,MISO,SCK。
打开全双工就行
使能,片选引脚
就正常的推挽输出,另外改一下名字(user label),我的代码都是按这名字写的
中断引脚
使能引脚EXTI,下降沿触发
开启NVIC
发送接收函数
HAL库的SPI发送接收函数需要的参数是数组,很麻烦,我们可以自己写一个
用到两个寄存器
状态寄存器里面的发送接收状态
比如发送,等到发送缓冲位空的时候再发送(没有数据再写)
接收则到接收缓冲不为空的时候再接收(有数据再读),读完以后,RXNE会自动清零
这个DR寄存器,对应发送和接收缓冲,具体怎么分的我不清楚
我分成两种写(标准库,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寄存器里面发送缓冲标志一直显示发送缓冲里有数据(数据发不出去,一直在寄存器里)
接收更不用说,没数据来过,接收缓冲标准一直是显示数据是空的
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有一组状态机
这个图从小马哥的视频里偷出来的,比较直观的看出几个状态怎么控制
数据包
si24r1基于包通讯,各种东西都是硬件实现的,我们使用只要配置一点东西
前导码不管
地址,比如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 位
接收端有6个接收通道,可以接6个设备的
看上面的图,接收方的6个通道的地址,P0是可以随便写的,P1-P5是高位共用的,最后8位是自己随便写。
SPI的时序图可以看到先发的是命令数据,下面的表是各种命令都是什么
一些基本代码
在初始化以前,我们先写两个函数用来操作芯片的寄存器;因为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;
}
设置模式(发送接收)
看表,发送模式就是配置0x0E(0000 1110),接收模式就是0x0F(00001111)
不过配置发送接收也就是最后一位,前面设置是我们用这里的CRC和中断
关断模式,待机模式我们都不考虑,都开机了哪有这两个模式的事
另外,因为有中断,切换模式的时候记得吧中断标志位去掉,下面寄存器0x7E(0111 1110)
只有两个状态要设置,所以参数是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工作模式
设置地址长度
就用5位的地址
设置TX节点地址
因为要配置ACK信号,所以在设置RX_ADDR_P0也就是RX接收地址的时候,要让两个地址相同
注意因为这里是低位先写入的,所以地址倒过来写。
比如
设置RX节点地址
上面设置TX说了,地址要相同
设置自动重发次数
随便设置,我这里是0x1A(0001 1010);重发延时是500u,重发次数是10次
使能PIPE0
使能ACK
设置通道0的数据宽度
我们用固定的数据长度32;也就是后面发送数据一定都是32位的
设置通讯频率
随便设置不过好像两个通讯的芯片要在一个信道
设置发射参数(低噪放大器增益,发射功率,无线速率)
设置大的发射功率,小的数据率,不动恒载波发射模式不用
设置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通讯(发送接收)
没用特殊操作就让模块一直在接收模式
当需要发数据的时候,再变成发送模式。
状态转换的函数上面已经写过了,我就是在这重复说一下
状态的转换我们再中断里面写,收发函数我们只实现接收和发送功能
虽然这么说,但是还要另外清除中断,不然会出事。
看表,读取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--);
}
外部中断
。。这个图片我放的就很SB,那就当补课吧,中断可以有什么函数可以来这里看
这个函数里面自带的函数HAL_GPIO_EXTI_IRQHandler会清空中断标志位,然后进入我们可以自己编写的HAL_GPIO_EXTI_Callback函数;IIC那里我们说过这个了,这里再提一句。
我们可以在it.c里面找到这个函数,在里面写
不过我个人喜欢把他删了,去自己文件下写,所以我剪切到nf03.c里面
这里提一个函数__HAL_GPIO_EXTI_GET_IT
__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) 外部中断处理的回调函数,需要用户重新实现
不太清楚为什么加这个,但大家都用他再中断函数里先判断一下是否还在中断
然后看着表我们往下写
先判断是不是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 */
}