STM32F767 SPI通信实验

SPI通信实验

SPI简介

串行外设接口(Serial Peripheral Interface Bus,SPI),是一种用于短程通信的同步串行通信接口规范,主要应用于单片机系统中。类似I²C。 这种接口首先被Motorola(摩托罗拉)公司开发,然后发展成了一种行业规范。典型应用包含SD卡和液晶显示器。 SPI设备之间使用全双工模式通信,包含一个主机和一个或多个从机。主机产生待读或待写的帧数据,多个从机通过一个片选线路 决定哪个来响应主机的请求。 有时SPI接口被称作四线程接口,SPI准确来讲称为同步串行接口,但是与同步串行接口协议(SSI)不同,SSI是一个四线程 同步通信协议,但是使用差分信号输入同时仅提供一个单工通信信道。

SPI总线规定了4个保留逻辑信号接口:

  • SCLK(Serial Clock):串行时钟,由主机发出
  • MOSI(Master Output,Slave Input):主机输出从机输入信号,由主机发出
  • MISO(Master Input,Slave Output):主机输入从机输出信号,由从机发出
  • SS(Slave Selected):选择信号,由主机发出,低电位有效

SPI总线:单一master对单一slave

STM32F767 SPI通信实验_第1张图片
单对单

SPI总线:单一master对复合slave

STM32F767 SPI通信实验_第2张图片
一对多

虽然上述名称是现在广泛在使用的,但是在有些情况下,比如旧的IC,可能与插图上描述的引脚口名称不同:

  • SCLK:SCK

  • MOSI:SIMO, SDO, DI, DIN, SI, MTSR

  • MISO:SOMI, SDI, DO, DOUT, SO, MRST

  • SDIO:SIO

  • SS: S̅S̅, SSEL, CS, C̅S̅, CE, nSS, /SS, SS#

MOSI / MISO惯例要求,在使用备用名称的设备上,主机上的SDI连接到从机上的SDI,反之亦然。从选择与芯片选择相同的功能,而不是寻址概念。引脚名称始终大写,如从选择,串行时钟和主输出从输入。

SPI Operation

SPI总线可以与单个主器件以及一个或多个从器件一起工作。

如果使用单个从器件,如果从器件允许,SS引脚可能会固定为逻辑低电平。有些器件需要下降边沿的片选信号来启动一个动作。一个例子是Maxim MAX1242 ADC,它在高→低转换时开始转换。对于多个从设备,每个从设备都需要从主站独立的SS信号。

大多数从器件具有三态输出,因此当未选择器件时,其MISO信号变为高阻抗(逻辑断开)。没有三态输出的器件不能与其他器件共享SPI总线段; 只有一个这样的从设备可以跟主设备说话。

数据传输

要开始通信,总线主机使用从设备支持的频率配置时钟,通常高达几MHz。然后主机在选择行上选择逻辑电平为0的从设备。如果需要等待时间,例如进行模数转换,则在发出时钟周期之前,主机必须至少等待一段时间。

在每个SPI时钟周期期间,发生全双工数据传输。主机在MOSI线路上发送一个位,从机读取它,而从机在MISO线路上发送一个位,主器件读取它。即使仅需要单向数据传输,也保持该序列。

传输通常涉及一些给定字大小的两个移位寄存器,例如8位,主器件中有1位,从器件中有1位。它们以虚拟环形拓扑连接。数据通常以最高有效位首先移出,同时将新的最低有效位移位到同一寄存器。同时,来自对方的数据被移入最低有效位寄存器。在寄存器位被移出和进入后,主器件和从器件已经交换了寄存器值。如果需要更换数据,则重新装入移位寄存器,重复该过程。传输可以持续任何数量的时钟周期。完成后,主站停止切换时钟信号,通常取消选择从站。

传输通常由8位字组成。然而,其他字大小也是常见的,例如,用于触摸屏控制器或音频编解码器的16位字,例如德州仪器公司的TSC2101 或12位字,用于许多数模转换器或模数转换器转换器。

总线上未使用芯片选择线激活的每个从器件都必须忽略输入时钟和MOSI信号,而不能驱动MISO。

典型的硬件设置使用两个移位寄存器来形成一个片间循环缓冲区,如下图:


STM32F767 SPI通信实验_第3张图片

时钟极性和相位

除了设置时钟频率外,主机还必须配置相对于数据的时钟极性和相位。摩托罗拉SPI指南将这两个选项分别命名为CPOL和CPHA,大多数厂商都采用了这一惯例。

时序图如下图所示。 该时序在下面进一步描述并适用于主设备和从设备。

显示时钟极性和相位的时序图。红线表示时钟前沿,蓝线,后沿。


STM32F767 SPI通信实验_第4张图片
  • CPOL确定时钟的极性。极性可以用简单的逆变器转换。
    • CPOL = 0是空闲0的时钟,每个周期由1的脉冲组成。也就是说,前沿是上升沿,后沿是下降沿。
    • CPOL = 1是空闲1的时钟,每个周期由0的脉冲组成。也就是说,前沿是下降沿,后沿是上升沿。
  • CPHA确定数据位相对于时钟脉冲的定时。在两种形式之间进行转换并不是微不足道的。
    • 对于CPHA = 0,“输出”侧在前一时钟周期的后沿更改数据,而“in”侧在时钟周期的前沿捕获(或不久之后)数据。外侧保持数据有效直到当前时钟周期的下降沿。对于第一个周期,第一个位必须位于MOSI线前面的前沿时钟边沿。
    • 考虑它的另一种方法是说,CPHA = 0周期由时钟空闲的半周期组成,随后是时钟被断言的半周期。
    • 对于CPHA = 1,“输出”侧在当前时钟周期的前沿更改数据,而“in”侧在时钟周期的后沿捕获(或不久之后)数据。外侧保持数据有效直到下一个时钟周期的前沿。对于上一个周期,从机将MISO线保持有效,直到从机选择被解除。
    • 考虑它的另一种方法是说,CHPA = 1周期由一个半周期组成,时钟被置位,随后是一个半周期,时钟空闲。

MOSI和MISO信号通常在半个周期内稳定(在其接收点),直到下一个时钟转换。SPI主从设备可能会在该半周期的不同点对数据进行采样。

这为主站和从站之间的通信通道增加了更多的灵活性。

模式

极性和相位的组合通常被称为根据以下约定通常编号的模式,CPOL为高位,CPHA为低位位:

对于“Microchip PIC”/“ARM”微控制器(注意,NCPHA是CPHA的反转):


STM32F767 SPI通信实验_第5张图片
极性

另一种常用的符号表示作为(CPOL,CPHA)元组的模式; 例如,值'(0,1)'将指示CPOL = 0和CPHA = 1。

中断

SPI器件有时使用另一条信号线将中断信号发送到主机CPU。实例包括从触摸屏传感器落笔中断,从温度传感器热限制警报,由实时时钟芯片,发出警报SDIO,和耳机插孔插入从在蜂窝电话的声音编解码器。中断不在SPI标准的范围内; 它们的使用既不被禁止也不被标准规定。

SPI接口

STM32F7 的 SPI 功能很强大, SPI 时钟最高可以到 54Mhz,支持 DMA,可以配置为 SPI
协议或者 I2S 协议(支持全双工 I2S)。
实验中,我们将使用 STM32F767 的 SPI 来驱动 NRF24L01 无线模块。这里对 SPI 我们只简
单介绍一下 SPI 的使用, STM32F767 的 SPI 详细介绍请参考《 STM32F7 中文参考手册》第 965页, 32 章。

我们使用 STM32F767 的 SPI2 来驱动 NRF24L01, HAL 库中 SPI 相关函数定义分布
在源文件stm32f7xx_hal_spi.c和对应的头文件stm32f7xx_hal_spi.h 中 。 下面就来看看STM32F767 的 SPI2 主模式配置步骤:

配置相关引脚的复用功能,使能SPI2时钟

我们要使用SPI2,第一步就是要使能SPI2时钟,SPI2的时钟通过APB1ENR的第14位来设置。其次要设置SPI2的相关引脚为复用(AF5)输出,这样才会连接到SPI2上。这里我们使用的是PB13,14,15这3个(SCK, MISO, MOSI, CS使用软件管理方式),所以设置这三个为复用IO,复用功能为AF5。

HAL库中,SPI2时钟使能方法:

__HAL_RCC_SPI2_CLK_ENABLE();    //使能SPI2时钟

IO口的复用功能设置前面我们已经多次提到,这里就不累赘了。和串口等其他外设一样,HAL库同样提供了SPI的初始化回调函数,用来编写与MCU相关的配置。我们只需要复现void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi);

初始化SPI2,设置SPI2工作模式等

这一步全部是通过SPI2_CR1寄存器来设置,我们设置SPI2为主机模式,设置数据格式为8位,然后通过CPOL和CPHA位来设置SCK时钟极性及采样方式。并设置SPI2的时钟频率(最大54MHz),以及数据的格式。在库函数中,初始化SPI的函数为:HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
该函数只有一个入口参数hspi,为SPI_HandleTypeDef结构体指针类型,该结构体定义如下:

typedef struct __SPI_HandleTypeDef
{
SPI_TypeDef         *Instance;          //外设寄存器基地址
SPI_InitTypeDef     Init;           //初始化结构体
uint8_t             *pTxBuffPtr;            //发送缓存
uint16_t            TxXferSize;             //发送数据大小
uint16_t            TxXferCount;            //还剩余多少个数据要发送
uint8_t             *pRxBuffPtr;            //接收缓存
uint16_t            RxXferSize;             //接收数据大小
uint16_t            RxXferCount;            //还剩余多少个数据要接收
DMA_HandleTypeDef   *hdmatx;        //DMA 发送句柄
DMA_HandleTypeDef   *hdmarx;        //DMA 接收句柄
void                (*RxISR)(struct __SPI_HandleTypeDef * hspi);
void                (*TxISR)(struct __SPI_HandleTypeDef * hspi);
HAL_LockTypeDef     Lock;
__IO HAL_SPI_StateTypeDef   State;
__IO uint32_t       ErrorCode;
}SPI_HandleTypeDef;

该结构体成员变量较多。成员变量 Instance 用来设置外设寄存器基地址,对于 SPI2,我们
设置为宏定义标识符 SPI2 即可。成员变量 pTxBuffPtr, TxXferSize 和 TxXferCount 用来设置 SPI发送缓存,发送数据量和发送剩余数据量。成员变量 pRxBuffPtr, RxXferSize 和 RxXferCount用来设置接收缓存,接收数据量和接收剩余数据量。hdmatx和 hdmarx 是 DMA 处理句柄。 RxISR 和 TxISR 是函数指针,用来指向 SPI 的接收和发送中断处理函数。这里我们着重讲解第二个成员变量 Init,该成员变量用来初始化 SPI时序和工作模式等,Init 成员变量是 SPI_InitTypeDef 结构体类型,该结构体定义如下:

typedef struct
{
uint32_t        Mode; // 模式:主( SPI_MODE_MASTER),从( SPI_MODE_SLAVE)
uint32_t        Direction; //方式: 只接受模式, 单线双向通信数据模式,全双工
uint32_t        DataSize; //8 位还是 16 位帧格式选择项
uint32_t        CLKPolarity; //时钟极性
uint32_t        CLKPhase; //时钟相位
uint32_t        NSS; //SS 信号由硬件( NSS 管脚)还是软件控制
uint32_t        BaudRatePrescaler; //设置 SPI 波特率预分频值
uint32_t        FirstBit; //起始位是 MSB 还是 LSB
uint32_t        TIMode; //帧格式 SPI motorola 模式还是 TI 模式
uint32_t        CRCCalculation; //硬件 CRC 是否使能
uint32_t        CRCPolynomial; //CRC 多项式
uint32_t        CRCLength;
uint32_t        NSSPMode;
}SPI_InitTypeDef;

结构体成员变量比较多, 接下来我们简单讲解一下:

  • 参数 Mode 用来设置 SPI 的主从模式,这里我们设置为主机模式 SPI_MODE_MASTER,当然有
    需要你也可以选择为从机模式 SPI_MODE_SLAVE。
  • 参数 Direction 用来设置 SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式,这里我们选择全双工模式 SPI_DIRECTION_2LINES。
  • 参数DataSize为 8位还是 16位帧格式选择项,这里我们是 8位传输,选择SPI_DATASIZE_8BIT。
  • 参数 CLKPolarity 用来设置时钟极性,我们设置串行同步时钟的空闲状态为高电平所以我们选
    择 SPI_POLARITY_HIGH。
  • 参数 CLKPhase 用来设置时钟相位, 也就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集,这里我们选择第二个跳变沿,所以选择SPI_PHASE_2EDGE。
  • 参数 NSS 设置 NSS 信号由硬件( NSS 管脚)还是软件控制,这里我们通过软件控制 NSS 关键,而不是硬件自动控制,所以选择 SPI_NSS_SOFT。
  • 参数 SPI_ BaudRatePrescaler 很关键,就是设置 SPI 波特率预分频值也就是决定 SPI 的时钟的参数 , 从2分频到256分频8个可选值,初始化的时候我们选择256分频值SPI_BAUDRATEPRESCALER_256, 传输速度为 108M/256=421.875KHz。
  • 参 数 FirstBit 设置数据传输顺序是MSB位在前还是LSB位在前,这里我们选择SPI_FIRSTBIT_MSB 高位在前。
  • 参数 TIMode 用来设置 TI 模式使能还是禁止,这里我们禁止即可。
  • 参数 CRCCalculation, CRCPolynomial 和 CRCLength 分别用来设置使能/禁止 CRC 校验, CRC校验多项式以及 CRC 校验的长度。
  • 参数 NSSPMode 用来设置在连续传输时,是否允许 SPI 在两个连续数据间产生 NSS 脉冲。

这些参数设置好后,就可以调用初始化函数了,设置示例如下:

SPI2_Handler.Instance=SPI2;             //SP2
SPI2_Handler.Init.Mode=SPI_MODE_MASTER; //设置 SPI 工作模式,设置为主模式
SPI2_Handler.Init.Direction=SPI_DIRECTION_2LINES;// SPI 设置为双线模式
SPI2_Handler.Init.DataSize=SPI_DATASIZE_8BIT; // SPI 发送接收 8 位帧结构
SPI2_Handler.Init.CLKPolarity=SPI_POLARITY_HIGH; //时钟的空闲状态为高电平
SPI2_Handler.Init.CLKPhase=SPI_PHASE_2EDGE; //同步时钟的第二个跳变沿数据采样
SPI2_Handler.Init.NSS=SPI_NSS_SOFT;     //内部 NSS 信号有 SSI 位控制
SPI2_Handler.Init.BaudRatePrescaler=SPI_BAUDRATEPRESCALER_256;//波特率 256 分频
SPI2_Handler.Init.FirstBit=SPI_FIRSTBIT_MSB; //数据传输从 MSB 位开始
SPI2_Handler.Init.TIMode=SPI_TIMODE_DISABLE; //关闭 TI 模式
SPI2_Handler.Init.CRCCalculation=SPI_CRCCALCULATION_DISABLE;//关闭 CRC 校验
SPI2_Handler.Init.CRCPolynomial=7;      //CRC 值计算的多项式

HAL_SPI_Init(&SPI2_Handler);            //初始化 SPI2

上面的代码备注有很详细的解释,前面也对这些参数进行了介绍。

使能SPI2

这一步通过SPI2_CR1的bit6来设置,以启动SPI2,在启动之后,我们就可以通过SPI通讯了,库函数使能SPI2的方法为:__HAL_SPI_ENABLE(&SPI2_Handler);

SPI传输数据

通信接口当然需要有发送数据和接收数据的函数,HAL库提供的发送/接收数据函数原型为:

HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);

这两个函数非常好理解,就是向SPIx数据寄存器中写/读数据Data,从而实现发送/接收。

前面我们简述了SPI的通信原理,因为SPI是全双工,发送一个字节同时接收一个字节,发送和接收同时完成,所有HAL库提供了一个发送接收统一函数:HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);
该函数发送一个字节的同时接收一个字节。

SPI中断处理

SPI1 和 SPI2 中断服务函数分别为 SPI1_IRQHandler 和 SPI2_IRQHandler,和串口中断处理
过程一样, HAL 库同样提供了 SPI 中断通用处理入口函数 HAL_SPI_IRQHandler,同时提供了
多个中断处理回调函数,通信过程各种中断最终都会通过相应的回调函数来处理。 SPI 相关回
调函数如下:

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi);//发送完成
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi);//接收完成
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);//发送接收完成
void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi);//发送过半
void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi);//接收过半
void HAL_SPI_TxRxHalfCpltCallback(SPI_HandleTypeDef *hspi);//发送接收过半
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi);//传输错误

中断的使能,及编程过程,这里就不累赘了,前面已经说的太多了。

SPI在HAL库当中的使用,就介绍到这里,接下来我们看下NRF24L01无线模块。

NRF24L01 无线模块简介

nRF24.L01是一款新型单片射频收发器件,工作于2.4 GHz~2.5 GHz ISM频段。内置频率合成器、功率放大器、晶体振荡器、调制器等功能模块,并融合了增强型ShockBurst技术,其中输出功率和通信频道可通过程序进行配置。nRF24L01功耗低,在以-6 dBm的功率发射时,工作电流也只有9 mA;接收时,工作电流只有12.3 mA,多种低功率工作模式,工作在100mw时电流为160mA,在数据传输方面实现相对WiFi距离更远,但传输数据量不如WiFi(掉电模式和空闲模式)使节能设计更方便。

功能介绍

  • 2.4Ghz 全球开放ISM 频段免许可证使
  • 最高工作速率2Mbps,高效GFSK调制,抗干扰能力强,特别适合工业控制场合
  • 126 频道,满足多点通信和跳频通信需要
  • 内置硬件CRC 检错和点对多点通信地址控制
  • 低功耗1.9-3.6V 工作,待机模式下状态为22uA;掉电模式下为900nA
  • 内置2.4Ghz 天线,体积小巧 15mmX29mm
  • 模块可软件设地址,只有收到本机地址时才会输出数据(提供中断指示),可直接接各种单片机使用,软件编程非常方便
  • 内置专门稳压电路,使用各种电源包括DC/DC 开关电源均有很好的通信效果
  • 2.54MM间距接口, DIP封装
  • 工作于EnhancedShockBurst 具有Automaticpackethandling,Auto packettransactionhandling,具有可选的内置包应答机制,极大的降低丢包率

硬件与接口

nRF24L01芯片

STM32F767 SPI通信实验_第6张图片
接口图示

STM32F767 SPI通信实验_第7张图片
接口介绍

nRF24L01模块

模块
  • VCC脚接电压范围为1.9V~3.6V之间,不能在这个区间之外,超过3.6V将会烧毁模块。推荐电压3.3V左右。
  • 除电源VCC和接地端,其余脚都可以直接和普通的5V单片机IO口直接相连,无需电平转换。当然对3V左右的单片机更加适用了。
  • 硬件上面没有SPI的单片机也可以控制本模块,用普通单片机IO口模拟SPI也可以。

各管脚位置和定义

  1. vcc:接1.9V~3.6V间的电压
  2. GND:接地
  3. CE:芯片的模式控制线。在 CSN 为低的情况下,CE 协同NRF24L01 的CONFIG 寄存器共同决定NRF24L01 的状态
  4. CSN:为芯片的片选线 CSN 为低电平芯片工作
  5. SCK:为芯片控制的时钟线(SPI时钟)
  6. MOSI:为芯片控制数据线,主设备数据输出 从设备数据输入
  7. MISO:芯片控制数据线,主设备数据输入 从设备数据输出
  8. IRQ:中断信号引脚。中断时变为低电平,即NRF24L01内部发生中断时IRQ引脚从高电平变为低电平。引脚会在以下三种情况变低:(1)Tx FIFO 发完并且收到ACK(使能ACK情况下);(2)Rx FIFO 收到数据;(3)达到最大重发次数。

工作模式

工作模式设定

STM32F767 SPI通信实验_第8张图片
工作模式
  • PWR_UP: 上电
  • PRIM_RX: 掉电
  • CE: 芯片使能

PWR_UP和PRIM_RX 在配置寄存器(CONFIG)中设置位0和位1:


STM32F767 SPI通信实验_第9张图片
寄存器

收发模式

收发模式有Enhanced(增强型) ShockBurstTM收发模式、ShockBurstTM收发模式和直接收发模式三种。

ShockBurstTM 模式

ShockBurstTM收发模式下,使用片内的先入先出堆栈区,数据低速从微控制器送入,但高速(1Mbps)发射,这样可以尽量节能,因此,使用低速的微控制器也能得到很高的射频数据发射速率。与射频协议相关的所有高速信号处理都在片内进行,这种做法有三大好处:尽量节能;低的系统费用(低速微处理器也能进行高速射频发射);数据在空中停留时间短,抗干扰性高。nRF2401 的 ShockBurstTM 技术同时也减小了整个系统的平均工作电流。

在 ShockBurstTM 收发模式下,nRF2401 自动处理字头和 CRC 校验码。在接收数据时,自动把字头和CRC 校验码移去。在发送数据时,自动加上字头和 CRC 校验码,当发送过程完成后,数据准备好引脚通知微处理器数据发射完毕。

ShockBurstTM收发模式可以与Nrf2401a,02,E1及E2兼容

Enhanced(增强型) ShockBurstTM 模式

增强型ShockBurst TM 典型的双链方式为:发送方要求终端设备在接收到数据后有应答信号,以便发送方检测有无数据丢失,一旦丢失则重发数据。重发数据设置在地址为 04 的数据重发设置寄存器 用于设置其重发次数及设置在未收到应答信号后等待重发的时间。

数据重发设置寄存器(SETUP_RETR):


STM32F767 SPI通信实验_第10张图片
数据重发设置寄存器

nRF24L01 在接收模式下可以接收6 路不同通道的数据。
每一个数据通道使用不同的地址,但是共用相同的频道。

也就是说6 个不同的nRF24L01 设置为发送模式后可以与同一个设置为接收模式的nRF24L01 进行通讯,而设置为接收模式的nRF24L01 可以对这6 个发射端进行识别。

数据通道0 是唯一的一个可以配置为40 位自身地址的数据通道。1~5 数据通道都为8 位自身地址和32 位公用地址。所有的数据通道都可以设置为增强型ShockBurst 模式。

NRF24L01在确认收到数据后记录地址,并以此地址为目标地址发送应答信号,在发送端,数据通道0被用作接收应答信号,因此属通道0 的接收地址要与发送地址端地址相等,以确保接收到正确的应答信号

STM32F767 SPI通信实验_第11张图片
传输示意图

当MCU控制NRF24L01发送数据时,NRF24L01就会启动发送数据,发送完后NRF24L01就会转到接收模式并等待终端的应答信号。

如果没有收到应答信号,NRF24L01就会重发数据包,直到收到应答信号,或达到重发次数寄存器设定的最大值为止,如果重发次数超过了设定值则产生MAX_RT(最大重发次数中断)(应该在该中断没有被屏蔽的情况下的时候才会发生)

只要收到确认信号,nRF24L01 就认为最后一包数据已经发送成功(接收方已经收到数据),把TX FIFO 中的数据清除掉并产生TX_DS中断(数据发送完中断)(IRQ 引脚置高)。

Enhanced ShockBurstTM发射流程
  1. 配置CONFIG寄存器位PRIM_RX 为低,进入发送模式

  2. 当MCU 有数据要发送时,接收节点地址(TX_ADDR)和有效数据(TX_PLD)通过SPI 接口写入nRF24L01。发送数据的长度以字节计数从MCU 写入TX FIFO。当CSN 为低时数据被不断的写入。发送端发送完数据后,将通道0 设置为接收模式来接收应答信号,其接收地址(RX_ADDR_P0)与接收端地址(TX_ADDR)相同。

    例:在上图 中数据通道5 的发送端(TX5)及接收端(RX)地址设置如下:
    TX5:TX_ADDR=0xB3B4B5B605
    TX5:RX_ADDR_P0=0xB3B4B5B605
    RX:RX_ADDR_P5=0xB3B4B5B605

  3. 把CE置高,最小时间为10us,激发nRF24L01进行Enhanced ShockBurstTM 发射

  4. nRF24L01 Enhanced ShockBurstTM 模式:

    • 无线系统上电
    • 频数据打包(加字头、 CRC校验码)
    • 高速发射数据包(由MCU 设定为1Mbps 或2Mbps)
  5. 如果启动了自动应答模式(自动重发计数器不等于0,ENAA_P0=1),无线芯片立即进入接收模式。如果在有效应答时间范围内收到应答信号,则认为数据成功发送到了接收端,此时状态寄存器的TX_DS 位置高并把数据从TX FIFO 中清除掉。如果在设定时间范围内没有接收到应答信号,则重新发送数据。如果自动重发计数器(ARC_CNT)溢出(超过了编程设定的值),则状态寄存器的MAX_RT 位置高。不清除TX FIFO 中的数据。当MAX_RT 或TX_DS 为高电平时IRQ 引脚产生中断。IRQ 中断通过写状态寄存器来复位(见中断章节)。如果重发次数在达到设定的最大重发次数时还没有收到应答信号的话,在MAX_RX 中断清除之前不会重发数据包。数据包丢失计数器(PLOS_CNT)在每次产生MAX_RT 中断后加一。也就是说:重发计数器ARC_CNT 计算重发数据包次数,PLOS_CNT 计算在达到最大允许重发次数时仍没有发送成功的数据包个数。

  6. 如果CE 置低,则系统进入待机模式I。如果不设置CE 为低,则系统会发送TX FIFO 寄存器中下一包数据。如果TX FIFO 寄存器为空并且CE 为高则系统进入待机模式II.

  7. 如果系统在待机模式II,当CE 置低后系统立即进入待机模式I.

#######相关寄存器:

自动应答寄存器:


STM32F767 SPI通信实验_第12张图片
自动应答寄存器

状态寄存器:


STM32F767 SPI通信实验_第13张图片
状态寄存器

发送检测寄存器:


STM32F767 SPI通信实验_第14张图片
发送检测寄存器
Enhanced ShockBurstTM接收流程
  1. 配置CONFIG寄存器位PRIM_RX 为高,进入接收模式准备接收数据的通道必须被使能(EN_RXADDR 寄存器),所有工作在增强型ShockBurstTM 模式下的数据通道的自动应答功能是由(EN_AA 寄存器)来使能的,有效数据宽度是由RX_PW_Px 寄存器来设置的。地址的建立过程见增强型ShockBurstTM 发送章节。
  2. 接收模式由设置CE 为高来启动。
  3. 130us 后nRF24L01 开始检测空中信息。
  4. 接收到有效的数据包后(地址匹配、CRC 检验正确),数据存储在RX_FIFO 中,同时RX_DR 位置高,并产生中断。状态寄存器中RX_P_NO 位显示数据是由哪个通道接收到的。
  5. 如果使能自动确认信号,则发送确认信号。
  6. MCU 设置CE 脚为低,进入待机模式I(低功耗模式)。
  7. MCU 将数据以合适的速率通过SPI 口将数据读出。
  8. 芯片准备好进入发送模式、接收模式或掉电模式。

#######相关寄存器

接收数据使能寄存器:


STM32F767 SPI通信实验_第15张图片
接收数据使能寄存器

接收数据通道有效数据宽度设置寄存器:


STM32F767 SPI通信实验_第16张图片
接收数据通道有效数据宽度设置寄存器
两种数据双方向的通讯方式

如果想要数据在双方向上通讯,PRIM_RX 寄存器必须紧随芯片工作模式的变化而变化。
处理器必须保证PTX 和PRX 端的同步性。在RX_FIFO 和TX_FIFO 寄存器中可能同时存有数据。

数据包识别和CRC校验应用于增强型ShockBurstTM模式下

每一包数据都包括两位的PID(数据包识别)来识别接收的数据是新数据包还是重发的数据包。

PID识别可以防止接收端同一数据包多次送入MCU。

在发送方每从MCU取得一包新数据后PID值加一。

PID和CRC校验应用在接收方识别接收的数据是重发的数据包还是新数据包。

如果链接中有一些数据丢失了,则PID值与上一包数据的PID值相同。

接收方对新接收的数据包进行比较:如果一包数据拥有与上一包数据相同的PID值,nRF24L01将对两包数据的CRC值进行比较。如果CRC值也相同的话就认为后面一包是前一包的重发数据包而被舍弃。

数据通道

nRF24L01 配置为接收模式时可以接收 6 路不同地址相同频率的数据。每个数据通道拥有自己的地址并且可以通过寄存器来进行分别配置。

数据通道是通过寄存器EN_RXADDR来设置的,默认状态下只有数据通道 0 和数据通道 1 是开启状态的。

每一个数据通道的地址是通过寄存器RX_ADDR_Px来配置的。通常情况下不允许不同的数据通道设置完全相同的地址。

数据通道040位可配置地址。数据通道1-5的地址为:32位共用地址+各自的地址(最低字节)。

下图所示的是数据通道1-5的地址设置方法举例。所有数据通道可以设置为多达40位,但是1-5数据通道的最低位必须不同。

当从一个数据通道接收到数据,并且此数据通道设置为应答方式的话,则nRF24L01 在收到数据后 产生应答信号,此应答信号的目标地址为接收通道地址

STM32F767 SPI通信实验_第17张图片
数据通道1-5的配置方式

待机模式

待机模式1在保证快速启动的同时减少系统平均消耗电流。

在待机模式1下,晶振正常工作。

在待机模式2下部分时钟缓冲器处在工作模式。

当发送端TX FIFO寄存器位空并且CE为高电平时进入待机模式2。

在待机模式期间,寄存器配置字内容保持不变。

掉电模式

在掉电模式下,nRF24L01各功能关闭,保持电流消耗最小。

进入掉电模式后,nRF24L01停止工作,但寄存器内容保持不变。

掉电模式由寄存器中PWR_UP位来控制。

涉及硬件

SPI实验涉及到的硬件就是前面介绍的SPI以及我们的无线NRF24L01模块,实验开始先检测无线模块,然后根据KEY0和KEY1按键来设置决定模块的工作模式,在设定好工作模式后,就会不停的发送/接收数据,需要的硬件如下:

  1. LED灯提示系统运行正常
  2. KEY0和KEY1按键
  3. LCD模块
  4. SPI2
  5. NRF24L01

NRF24L01属于外接模块,这里我们仅介绍下开发板上的模块接口和STM32F767的连接情况,他们的连接关系如下图:


STM32F767 SPI通信实验_第18张图片
连接原理图

这里无线模块连接的是SPI2,连接在PB13,14,15这3个IO口上,看图上可以看到 NRF_IRQ和GBC_KEY共用了PI11, NRF_CE和SPDIF_RX共用PG12 所以,他们不能同时使用,需要分时复用。由于无线模块需要双向通信,所有至少需要两个模块同时工作,所以这里需要大家用两套开发板和无线模块来测试。

软件设计

打开工作,我们添加spi.c以及nrf24l01.c两个文件,编写我们的SPI以及NRF24L01的底层驱动函数,当然,还要添加对应的头文件。首先来编写spi.c。

spi.c

SPI_HandleTypeDef SPI2_Handler;         //定义一个全局的SPI2句柄

void SPI2_Init(void)                    //SPI2初始化,并配置为主机模式
{
    SPI2_Handler.Instance   = SPI2;     //SPI2
    SPI2_Handler.Init.Mode  = SPI_MODE_MASTER;  //设置SPI2为主模式
    SPI2_Handler.Init.Direction = SPI_DIRECTION_2LINES; //SPI设置为双线模式
    SPI2_Handler.Init.DataSize  = SPI_DATASIZE_8BIT;    //SPI发送接收8bit结构
    SPI2_Handler.Init.CLKPolarity   = SPI_POLARITY_HIGH;    //同步时钟空闲状态为高电平
    SPI2_Handler.Init.CLKPhase      = SPI_PHASE_2EDGE;      //同步时钟第二个跳变沿采样数据
    SPI2_Handler.Init.NSS           = SPI_NSS_SOFT;         //内部NSS型号有SSI位控制
    SPI2_Handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;    //定义波特率预分频值为256
    SPI2_Handler.Init.FirstBit  = SPI_FIRSTBIT_MSB;         //数据传输从MSB位开始
    SPI2_Handler.Init.TIMode    = SPI_TIMODE_DISABLE;       //关闭TI模式
    SPI2_Handler.Init.CRCCalculation    = SPI_CRCCALCULATION_DISABLE;   //关闭硬件CRC检测
    SPI2_Handler.Init.CRCPolynomial     = 7;                //CRC值计算的多项式
    HAL_SPI_Init(&SPI2_Handler);
    __HAL_SPI_ENABLE(&SPI2_Handler);                    //使能SPI2
    SPI2_ReadWriteBtye(0xff);                           //启动传输
}

这份代码在上边以上已经介绍了一边,给句柄用宏赋值,本质就是在配置SPI的功能寄存器,这个前面已经说的太多了,这里就不累赘了,配置完了以后调用HAL_SPI_Init()来初始化,并且要使能SPI,然后开启传输。

那么我们知道HAL_SPI_Init()函数在内部会调用一个回调函数,我们需要来实现他,配置引脚以及时钟使能:

void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE();           //使能 GPIOB 时钟
__HAL_RCC_SPI2_CLK_ENABLE();            //使能 SPI2 时钟

//PB13,14,15
GPIO_Initure.Pin=GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15;
GPIO_Initure.Mode=GPIO_MODE_AF_PP;      //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP;          //上拉
GPIO_Initure.Speed=GPIO_SPEED_FAST;     //快速
GPIO_Initure.Alternate=GPIO_AF5_SPI2;   //复用为 SPI2
HAL_GPIO_Init(GPIOB,&GPIO_Initure);     //初始化
}

代码非常简单,就不多做解释了,STM32F767上面的SPI的分频系数是2~256,那么我们来封装一个函数,随需要动态调整SPI的通信速度,SPI速度 = fAPB1 / f分频系数,HAL库中对分频系数的宏定义是SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_256,fAPB1 时钟一般为 54Mhz,代码如下:

void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));//判断有效性
__HAL_SPI_DISABLE(&SPI2_Handler);               //关闭 SPI
SPI2_Handler.Instance->CR1&=0XFFC7;             //位 3-5 清零,用来设置波特率
SPI2_Handler.Instance->CR1|=SPI_BaudRatePrescaler;  //设置 SPI 速度
__HAL_SPI_ENABLE(&SPI2_Handler);                //使能 SPI
}

函数入口处,使用了断言函数,来确定参数的合法性,如果不符合HAL库定义的参数取值范围,就会自动返回,不会继续执行,这个前面也已经说过很多次了。如果要通过SPI读写,再封装一个函数来调用HAL库函数即可,代码如下:

u8 SPI2_ReadWriteByte(u8 TxData)
{
u8 Rxdata;
HAL_SPI_TransmitReceive(&SPI2_Handler,&TxData,&Rxdata,1, 1000);
return Rxdata;              //返回收到的数据
}

SPI2_ReadWriteByte 函数主要是通过调用 HAL 库中 SPI 发送接收函数 HAL_SPI_TransmitReceive 来实现数据的发送和接收。spi.c的底层函数就这么多了,接下来我们来看下nrf24l01的底层驱动函数封装。

nrf24l01.c

const u8 TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址
const u8 RX_ADDRESS[RX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //接收地址

首先定义两个全局const修饰的数组,来存放发送和接收地址。然后针对我们的NRF24L01来修改我们的spi2驱动,代码如下:

void NRF24L01_SPI_Init(void)
{
__HAL_SPI_DISABLE(&SPI2_Handler);               //先关闭 SPI2
SPI2_Handler.Init.CLKPolarity=SPI_POLARITY_LOW; //同步时钟的空闲状态为低电平
SPI2_Handler.Init.CLKPhase=SPI_PHASE_1EDGE;     //同步时钟第 1 个跳变沿采样数据
HAL_SPI_Init(&SPI2_Handler);
__HAL_SPI_ENABLE(&SPI2_Handler);                //使能 SPI2
}

我们的NRF24L01需要8根线来连接,除过电源线VCC和GND外,之前SPI通信已经初始化过了SCK,MOSI,MISO三根线,要让NRF24L01通信还需要连接其他3根(CE,CSN,IRQ),具体代码如下:

void NRF24L01_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOG_CLK_ENABLE(); //开启 GPIOG 时钟
__HAL_RCC_GPIOI_CLK_ENABLE(); //开启 GPIOI 时钟

GPIO_Initure.Pin=GPIO_PIN_10|GPIO_PIN_12; //PG10,12
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOG,&GPIO_Initure); //初始化

GPIO_Initure.Pin=GPIO_PIN_11; //PI11
GPIO_Initure.Mode=GPIO_MODE_INPUT; //输入
HAL_GPIO_Init(GPIOI,&GPIO_Initure); //初始化

SPI2_Init(); //初始化 SPI2
NRF24L01_SPI_Init(); //针对 NRF 的特点修改 SPI 的设置
NRF24L01_CE(0); //使能 24L01
NRF24L01_CSN(1); //SPI 片选取消
}

通过前面的连接图,我们知道这3根线连接的是PG10,12以及PI11,按照HAL库IO口初始化套路设置就行,然后调用了SPI2_Init()函数,初始化SPI2的IO口,并且调用NRF24L01_SPI_Init()函数,针对NRF修改SPI设置,后面调用了2个宏函数,定义在头文件当中,具体定义如下:

#define NRF24L01_CE(n) (n?HAL_GPIO_WritePin(GPIOG,GPIO_PIN_12,\
GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOG,GPIO_PIN_12,GPIO_PIN_RESET))  
//24L01 片选信号
#define NRF24L01_CSN(n) (n?HAL_GPIO_WritePin(GPIOG,GPIO_PIN_10,\
GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOG,GPIO_PIN_10,GPIO_PIN_RESET))  
//SPI 片选信号

依然通过三目运算符来控制CE线以及CSN输出高低电压。这些初始化完成后,之前介绍过了,NRF24L01芯片内部还有他自己的寄存器,需要通过SPI来设置,那么还需要封装2个函数来对NRF24L01芯片内部寄存器来进行读写,具体代码如下:

u8 NRF24L01_Write_Reg(u8 reg,u8 value)
{
u8 status;
NRF24L01_CSN(0);                    //使能 SPI 传输
status =SPI2_ReadWriteByte(reg);    //发送寄存器号
SPI2_ReadWriteByte(value);          //写入寄存器的值
NRF24L01_CSN(1);                    //禁止 SPI 传输
return(status);                     //返回状态值
}

u8 NRF24L01_Read_Reg(u8 reg)
{
u8 reg_val;
NRF24L01_CSN(0);                    //使能 SPI 传输
SPI2_ReadWriteByte(reg);            //发送寄存器号
reg_val=SPI2_ReadWriteByte(0XFF);   //读取寄存器内容
NRF24L01_CSN(1);                    //禁止 SPI 传输
return(reg_val);                    //返回状态值
}

这2个函数就非常简单了,调用了HAL库内部的SPI操作函数,先开启SPI传输,然后发送要读写内容的寄存器的地址,然后再次调用SPI_ReadWriteByte()函数,就可以得到该寄存器的值,用一个变量取接返回值就行,然后关闭SPI传输。

要使用NRF24L01通信当然是发送数据了,那么封装两个函数在指定位置(寄存器)读写指定长度的数据,代码如下:

u8 NRF24L01_Read_Buf(u8 reg,u8 *pBuf,u8 len)
{
u8 status,u8_ctr;
NRF24L01_CSN(0);                    //使能 SPI 传输
status=SPI2_ReadWriteByte(reg);     //发送寄存器值(位置),并读取状态值
for(u8_ctr=0;u8_ctr

两个函数长的非常相像,都是先通过SPI发送需要读/写的位置(寄存器),然后再调用一次SPI2_ReadWriteByte()函数,就可以读/写数据了。

基本工作做完了,现在可以调用上面封装的函数来编写函数发送/接收一次数据了,代码如下:

u8 NRF24L01_TxPacket(u8 *txbuf)
{
u8 sta;
SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8);     //spi 速度为 6.75Mhz
NRF24L01_CE(0);
NRF24L01_Write_Buf(WR_TX_PLOAD,txbuf,TX_PLOAD_WIDTH);/
NRF24L01_CE(1);                             //启动发送
while(NRF24L01_IRQ!=0);                     //等待发送完成
sta=NRF24L01_Read_Reg(STATUS);              //读取状态寄存器的值
NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS,sta);
if(sta&MAX_TX)  {                           //达到最大重发次数
    NRF24L01_Write_Reg(FLUSH_TX,0xff);      //清除 TX FIFO 寄存器
    return MAX_TX;
}
if(sta&TX_OK) {                             //发送完成
    return TX_OK;
}
return 0xff;                                //其他原因发送失败
}

u8 NRF24L01_RxPacket(u8 *rxbuf)
{
u8 sta;
SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8);
sta=NRF24L01_Read_Reg(STATUS);              //读取状态寄存器的值
NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS,sta);
if(sta&RX_OK) {                             //接收到数据
    NRF24L01_Read_Buf(RD_RX_PLOAD,rxbuf,RX_PLOAD_WIDTH);    //读取数据
    NRF24L01_Write_Reg(FLUSH_RX,0xff);      //清除 RX FIFO 寄存器
    return 0;
}
return 1;                                   //没收到任何数据
}

在头文件中,有如下定义:

//NRF24L01寄存器操作命令
#define NRF_READ_REG    0x00  //读配置寄存器,低5位为寄存器地址
#define NRF_WRITE_REG   0x20  //写配置寄存器,低5位为寄存器地址
#define RD_RX_PLOAD     0x61  //读RX有效数据,1~32字节
#define WR_TX_PLOAD     0xA0  //写TX有效数据,1~32字节
#define FLUSH_TX        0xE1  //清除TX FIFO寄存器.发射模式下用
#define FLUSH_RX        0xE2  //清除RX FIFO寄存器.接收模式下用
#define REUSE_TX_PL     0xE3  //重新使用上一包数据,CE为高,数据包被不断发送.
#define NOP             0xFF  //空操作,可以用来读状态寄存器   

//SPI(NRF24L01)寄存器地址
#define CONFIG          0x00  //配置寄存器地址;bit0:1接收模式,0发射模式;bit1:电选择;bit2:CRC模式;bit3:CRC使能;
//bit4:中断MAX_RT(达到最大重发次数中断)使能;bit5:中断TX_DS使能;bit6:中断RX_DR使能
#define EN_AA           0x01  //使能自动应答功能  bit0~5,对应通道0~5
#define EN_RXADDR       0x02  //接收地址允许,bit0~5,对应通道0~5
#define SETUP_AW        0x03  //设置地址宽度(所有数据通道):bit1,0:00,3字节;01,4字节;02,5字节;
#define SETUP_RETR      0x04  //建立自动重发;bit3:0,自动重发计数器;bit7:4,自动重发延时 250*x+86us
#define RF_CH           0x05  //RF通道,bit6:0,工作通道频率;
#define RF_SETUP        0x06  //RF寄存器;bit3:传输速率(0:1Mbps,1:2Mbps);bit2:1,发射功率;bit0:低噪声放大器增益
#define STATUS          0x07  //状态寄存器;bit0:TX FIFO满标志;bit3:1,接收数据通道号(最大:6);bit4,
//达到最多次重发;bit5:数据发送完成中断;bit6:接收数据中断;

#define MAX_TX          0x10  //达到最大发送次数中断
#define TX_OK           0x20  //TX发送完成中断
#define RX_OK           0x40  //接收到数据中断

#define OBSERVE_TX      0x08  //发送检测寄存器,bit7:4,数据包丢失计数器;bit3:0,重发计数器
#define CD              0x09  //载波检测寄存器,bit0,载波检测;
#define RX_ADDR_P0      0x0A  //数据通道0接收地址,最大长度5个字节,低字节在前
#define RX_ADDR_P1      0x0B  //数据通道1接收地址,最大长度5个字节,低字节在前
#define RX_ADDR_P2      0x0C  //数据通道2接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P3      0x0D  //数据通道3接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P4      0x0E  //数据通道4接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P5      0x0F  //数据通道5接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define TX_ADDR         0x10  //发送地址(低字节在前),ShockBurstTM模式下,RX_ADDR_P0与此地址相等
#define RX_PW_P0        0x11  //接收数据通道0有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P1        0x12  //接收数据通道1有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P2        0x13  //接收数据通道2有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P3        0x14  //接收数据通道3有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P4        0x15  //接收数据通道4有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P5        0x16  //接收数据通道5有效数据宽度(1~32字节),设置为0则非法
#define NRF_FIFO_STATUS 0x17  //FIFO状态寄存器;bit0,RX FIFO寄存器空标志;bit1,RX FIFO满标志;bit2,3,保留;
//bit4,TX FIFO空标志;bit5,TX FIFO满标志;bit6,1,循环发送上一数据包.0,不循环;

#define NRF24L01_IRQ HAL_GPIO_ReadPin(GPIOI,GPIO_PIN_11)        //IRQ 主机数据输入

这里定义非常多的宏,看着很麻烦,但是在工程当中,这样做是非常有必要的,可以节省大量的代码量,后期在RAM开发中,大家就会遇到,基本每个模块都需要在头文件中去这样来定义。这里还通过 TX_PLOAD_WIDTH 和 RX_PLOAD_WIDTH 决定了发射和接收的数据宽度,也就是我们每次发射和接受的有效字节数。 NRF24L01 每次最多传输 32 个字节,再多的字节传输则需要多次传送。

还需要有两个函数来对REF24L01的模式来进行切换,代码如下:

void NRF24L01_RX_Mode(void)
{
NRF24L01_CE(0);
NRF24L01_Write_Buf(NRF_WRITE_REG+RX_ADDR_P0,(u8*)RX_ADDRESS,
RX_ADR_WIDTH);                                      //写 RX 节点地址
NRF24L01_Write_Reg(NRF_WRITE_REG+EN_AA,0x01);       //使能通道 0 的自动应答
NRF24L01_Write_Reg(NRF_WRITE_REG+EN_RXADDR,0x01);   //使能通道 0 的接收地址
NRF24L01_Write_Reg(NRF_WRITE_REG+RF_CH,40);         //设置 RF 通信频率
NRF24L01_Write_Reg(NRF_WRITE_REG+RX_PW_P0,RX_PLOAD_WIDTH);
//选择通道 0 的有效数据宽度
NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP,0x0f);    //设置 TX 发射参数,0db 增益,2Mbps,低噪声增益开启
NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG, 0x0f);     //配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式
NRF24L01_CE(1);                                     //CE 为高,进入接收模式
}

void NRF24L01_TX_Mode(void)
{
NRF24L01_CE(0);
NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR,(u8*)TX_ADDRESS,
TX_ADR_WIDTH);                                      //写 TX 节点地址
NRF24L01_Write_Buf(NRF_WRITE_REG+RX_ADDR_P0,(u8*)RX_ADDRESS,RX_ADR_WIDTH);  //设置 TX 节点地址,主要为了使能 ACK
NRF24L01_Write_Reg(NRF_WRITE_REG+EN_AA,0x01);       //使能通道 0 的自动应答
NRF24L01_Write_Reg(NRF_WRITE_REG+EN_RXADDR,0x01);   //使能通道 0 接收地址
NRF24L01_Write_Reg(NRF_WRITE_REG+SETUP_RETR,0x1a);  //设置自动重发间隔时间:500us + 86us;最大自动重发次数:10 次
NRF24L01_Write_Reg(NRF_WRITE_REG+RF_CH,40);         //设置 RF 通道为 40
NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP,0x0f);    //设置 TX 发射参数,0db 增益,2Mbps,低噪声增益开启
NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG,0x0e);      //配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断
NRF24L01_CE(1);                                     //CE 为高,10us 后启动发送
}

这个两个函数都是根据NRF24L01的设定来配置寄存器,初始化NRF24L01到接收或者发送模式,并相应的设置其他配置。

然后再封装一个函数来动态监测NRF24L01是否存在,代码如下:

u8 NRF24L01_Check(void)
{
u8 buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5};
u8 i;
SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8);             //spi 速度为 6.75Mhz
NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR,buf,5);    //写入 5 个字节的地址.
NRF24L01_Read_Buf(TX_ADDR,buf,5);                   //读出写入的地址
for(i=0;i<5;i++) {
    if(buf[i]!=0XA5)break;
}
if(i!=5) return 1;                                  //检测 24L01 错误

return 0;                                           //检测到 24L01
}

以上就是24l01的底层驱动代码了,完成了对NRF24L01的初始化,模式设置(发送/接收),数据读写等操作。在这里强调一个要注意的地方,在NRF24L01_Init()函数里面,我们调用了SPI2_Init()函数,该函数设置的是SCK空闲时为高,但是NRF24L01的SPI通信时序如下图:

STM32F767 SPI通信实验_第19张图片
NRF24L01时序图

上图中Cn代表指令位,Sn代表状态寄存器位,Dn代表数据位。从图中可以看出,SCK空闲的时候是低电平,而数据在SCK的上升沿被读写。所有,我们在 NRF24L01_Init()函数里面又单独添加了将CPOL和CPHA设置为0的函数 NRF24L01_SPI_Init()。这里主要是修改了下面两行代码:

SPI2_Handler.Init.CLKPolarity=SPI_POLARITY_LOW; //串行同步时钟的空闲状态为低电平
SPI2_Handler.Init.CLKPhase=SPI_PHASE_1EDGE; //同步时钟的第 1 个跳变沿数据被采样

你可能感兴趣的:(STM32F767 SPI通信实验)