文章目录

    • 【实验目的】
    • 【实验原理】
      • 一、SPI原理
      • 二、SPI特性
      • 三、SPI库函数分析
      • 四、蓝牙模块NRF2401
      • 五、软件流程图
    • 【实验环境】
      • 操作系统
      • 硬件设备
      • 软件
    • 【实验步骤】
      • 一、配置工程环境
      • 二、开启时钟,完成端口初始化
      • 三、编写SPI模块
      • 四、编写main函数和控制模块
      • 五、编译并下载程序到小车。
    • 【实验思考】
      • 一、选择题
      • 二、简答题
    • 附录:SPI 库函数

 


【实验目的】

1、通过实验了解SPI的通信模式及配置过程。
2、通过使用SPI与蓝牙模块NRF2401进行通信,送内部数据到蓝牙模块并读取从蓝牙主机上发送的控制信息,了解蓝牙模块的配置和通信过程。

【实验原理】

SPI 协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线,它由摩托罗拉公司提出,当前最新的为 V04.01-2004 版。它被广泛地使用在 ADC、 LCD 等设备与 MCU 间通讯的场合。SPI通讯设备之间常用连接方式如图1所示。
嵌入式实验 之 SPI通信实验_嵌入式
图1 常见的SPI通讯系统

一、SPI原理

SPI 接口一般使用 4 条线: MISO 主设备数据输入,从设备数据输出。 MOSI 主设备数据输出,从设备数据输入SCLK 时钟信号,由主设备产生。CS 从设备片选信号,由主设备控制。根据 SPI 时钟极性(CPOL)和时钟相位(CPHA) 配置的不同,分为四种 SPI 模式。时钟极性是指 SPI 通讯设备处于空闲状态时SCK 信号线的电平信号。时钟相位是指数据的采样的时刻。

二、SPI特性

SPI 主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作; 提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。
SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果 CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果 CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI 主模块和与之通信的外设备时钟相位和极性应该一致。

三、SPI库函数分析

跟其他外设一样,STM32标准库提供了SPI初始化结构体及初始化函数来配置SPI外设。初始化结构体及函数定义在库文件“stm32f4xx_spi.h”和“stm32f4xx_spi.c”中。
SPI初始化结构体为SPI_InitTypeDef,其中包含:
嵌入式实验 之 SPI通信实验_嵌入式实验_02
表1 SPI_InitTypeDef配置

配置完这些结构体成员后,我们要调用库函数:
SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)
把这些参数写入到寄存器中,实现 SPI 的初始化,然后调用库函数:SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState) 来使能 SPI 外设。

在进行SPI发送数据时我们需要用到库函数:SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
来判断指定的SPI的标志位,在本实验中,检查指定的SPI标志位设置与否:发送缓存空标志位。
SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)来发送指定的数据。

在进行SPI接受数据时我们需要用到库函数:
SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
来判断指定的SPI的标志位,在本实验中,检查指定的SPI标志位设置与否:接受缓存非空标志位。
SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)通过SPIx最近接受的数据。

四、蓝牙模块NRF2401

nRF24L01是一款工作在2.4~2.5GHz世界通用ISM频段的单片无线收发器芯片。无线收发器包括:频率发生器、增强型SchockBurst模式控制器、功率放大器、晶体振荡器、调制器、解调器、输出功率、频道选择和协议的设置可以通过SPI接口进行设置。极低的电流消耗:当工作在发射模式下发射功率为-6dBm时电流消耗为9.0mA,接收模式时为12.3mA,掉电模式和待机模式下电流消耗更低。

五、软件流程图

嵌入式实验 之 SPI通信实验_物联网_03
图2 程序流程图

【实验环境】

操作系统

Windows7/8/10,32bit/64bit

硬件设备

小车所搭载的电路板主控芯片留有蓝牙调试端口,可以通过SPI连接蓝牙设备发送数据进行调试。

软件

Keil 5,串口助手软件

【实验步骤】

一、配置工程环境

1.1 在操作之前需要把关于GPIO,SPI,USART等的库文件添加到工程模板之中。在添加这些库文件之前需要把与stm32f10x_xxx.c 文件对应的一个 stm32f10x_xxx.h 头文也包含进我们的工程中才能够使用这些外设库。如图3所示。
嵌入式实验 之 SPI通信实验_嵌入式_04
图3 所需的头文件

二、开启时钟,完成端口初始化

2.1 打开程序中的spi.c文件,对SPI1_init函数进行编写和修改。在这个函数中我们调用了库函数 RCC_APB2PeriphClockCmd()初始化SPI1 和 GPIOC 的时钟。
2.2 GPIO端口时钟初始化

	/*对GPIOC端口进行初始化设置*/
GPIO_InitTypeDef GPIO_InitStructure;   
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE );	//初始化时钟

2.3 GPIO 端口模式设置

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;             //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;            //GPIO速率
GPIO_Init(GPIOA, &GPIO_InitStructure);                        //GPIO初始化
GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7);

2.4 对SPI进行初始化配置,使用SPI初始化结构体。

SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; 
    //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工	      
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
//设置SPI工作模式:设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
//设置SPI的数据大小:SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
//选择了串行时钟的稳态:时钟悬空高
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
//数据捕获于第二个时钟沿
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;	
//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
SPI_InitStructure.SPI_BaudRatePrescaler = 					 SPI_BaudRatePrescaler_256	
//定义波特率预分频的值:波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; 
    //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始     
SPI_InitStructure.SPI_CRCPolynomial = 7;
	//CRC值计算的多项式
SPI_Init(SPI1, &SPI_InitStructure);
   //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器,这里我们初始化的是  SPI1 

2.5 使能SPI。

SPI_Cmd(SPI1, ENABLE);
//使能SPI外设SPI1
	SPI1_ReadWriteByte(0xff);
//启动传输	

2.6 设置SPI的传输速率。
嵌入式实验 之 SPI通信实验_嵌入式实验_05
图4 SPI控制寄存器1结构图
通过SPI的控制寄存器1设置SPI的传输速率,如图4所示

void SPI1_SetSpeed(u8 SpeedSet)
{
	SPI1->CR1&=0XFFC7;      //将第3位,第4位,第5位清零
SPI1->CR1|=SpeedSet;	     //设置SPI1速度  
	SPI1->CR1|=1<<6; 		//SPI设备使能 
}

2.7 编写SPI读写字节函数。

//SPIx 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI1_ReadWriteByte(u8 TxData)
{		
		u8 retry=0;				 	
		while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) 
		  //检查指定的SPI标志位设置与否:发送缓存空标志位
		 {
			  retry++;
			  if(retry>200)	return 0; //超时退出
		 }	//判断数据寄存器中是否有数据,若没有,则写入我们的数据
		  SPI_I2S_SendData(SPI1, TxData);   //通过外设SPIx发送一个数据
		  retry=0;
		  while (SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)== RESET)
		  //检查指定的SPI标志位设置与否:接受缓存非空标志位
		   {
			  retry++;
		  	  if(retry>200)	return 0;  //超时退出
	}	  	//判断数据寄存器中是否有数据,若有,则将其读取出来   
 return SPI_I2S_ReceiveData(SPI1); //返回通过SPIx最近接收的数据
}

在24L01.c中编辑传输函数。
因为24L01程序较为复杂,建议使用参考例程。仅做初始化配置。

三、编写SPI模块

3.1 初始化24L01的IO口。

//初始化24L01的IO口
void NRF24L01_Init(void)
{  
		GPIO_InitTypeDef GPIO_InitStructure;
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|
RCC_APB2Periph_GPIOC, ENABLE );		    //PC5端口设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode =GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_5);
		//PB12端口设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode =GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); 
GPIO_SetBits(GPIOB,GPIO_Pin_12);
		//PC4端口设置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ;  //上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_4);
}
  // NRF24L01_CE和NRF24L01_CSN的定义在24l01.h中
	NRF24L01_CE=0; 	//使能24L01
		NRF24L01_CSN=1;	//SPI片选取消	 		

3.2编写基于SPI的24L01读写函数。

//SPI写寄存器
//reg:指定寄存器地址
//value:写入的值
u8 NRF24L01_Write_Reg(u8 reg,u8 value)
{
	u8 status;	
     NRF24L01_CSN=0;           //使能SPI传输
  	    status =SPI1_ReadWriteByte(reg);//发送寄存器号 
  	    SPI1_ReadWriteByte(value);    //写入寄存器的值
  	    NRF24L01_CSN=1;           //禁止SPI传输	   
  	    return(status);       			//返回状态值
}
//读取SPI寄存器值
//reg:要读的寄存器
u8 NRF24L01_Read_Reg(u8 reg)
{
		    u8 reg_val;	    
 	    NRF24L01_CSN = 0;             //使能SPI传输		
  	    SPI1_ReadWriteByte(reg);         //发送寄存器号
  	    reg_val=SPI1_ReadWriteByte(0XFF);//读取寄存器内容
  	    NRF24L01_CSN = 1;             //禁止SPI传输		    
  	    return(reg_val);                  //返回状态值
}	

四、编写main函数和控制模块

通过蓝牙遥控器控制小车
4.1 控制语句编写在程序control.c文件中。因为在24L01和按键程序中已经对输入信号做了处理,因此只需判断蓝牙发送的数据是何种控制命令即可。

/************************************************************
函数功能:采集遥控器的信号
入口参数:无
返回  值:无
************************************************************/
void  Get_MC6(void)
{ 
		if(Flag_Left==0&&Flag_Right==0) //判断左转和右转标志位是否为零
		{	
			if((Remoter_Ch1>1650&&Remoter_Ch1<2100)
||(Remoter_Ch1>21650&&Remoter_Ch1<22100))	
Flag_Qian=1,Flag_Hou=0,Flag_sudu=1;
//判断遥控接收变量Remoter_Ch1
				//前进
else if((Remoter_Ch1<1350&&Remoter_Ch1>900) 										||(Remoter_Ch1<21350&&Remoter_Ch1>20900))
Flag_Qian=0,Flag_Hou=1,Flag_sudu=1;
				//后退
else if ((Remoter_Ch1>1350&&Remoter_Ch1<1650) 										||(Remoter_Ch1>21350&&Remoter_Ch1<21650))		Flag_Qian=0,Flag_Hou=0;
				//停
}
if(Flag_Qian==0&&Flag_Hou==0)//判断前进和后退标志位是否为零
{			
if((Remoter_Ch2>1650&&Remoter_Ch2<2100)
||(Remoter_Ch2>21650&&Remoter_Ch2<22100))
Flag_Left=1,Flag_Right=0,Flag_sudu=1;
//判断遥控接收变Remoter_Ch2
				//左转
else if((Remoter_Ch2<1350&&Remoter_Ch2>900) 										||(Remoter_Ch2<21350&&Remoter_Ch2>20900))
Flag_Left=0,Flag_Right=1,Flag_sudu=1;
				//右转
else if ((Remoter_Ch2>1350&&Remoter_Ch2<1650) 										||(Remoter_Ch2>21350&&Remoter_Ch2<21650))
Flag_Left=0,Flag_Right=0;
				//停
			}	
}	

五、编译并下载程序到小车。

嵌入式实验 之 SPI通信实验_编程环境_06
图5 Keil编译环境下的下载按键

【实验思考】

一、选择题

题目1:下面哪条语句是设置SPI通讯数据的大小(D)
A:SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
B:SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
C:SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
D:SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;

题目2:在对SPI进行初始化时,涉及到的端口应该设置成哪种模式?(A)
A:复用推挽输出
B:复用开漏输出

二、简答题

题目1:SPI的片选线(CS)设置是必需的吗?

CS线用于控制片选信号。当一个SPI从设备的CS线识别到了预先规定的片选电平,则表示该设备被选中,接下来的操作对其有效。显然,使用CS线可以完成“一主多从”的SPI网络架设,但是,在“一主一从”的SPI通信时,CS线不是必需的。

附录:SPI 库函数

嵌入式实验 之 SPI通信实验_物联网_07