航模程序设计
简介
由于目前还没有绘制PCB,因此采用的是最小系统和模块进行拼接的方式测试程序。
硬件部分:
测试核心板:接收机:STM32F407VGT6核心板。遥控:正点原子精英板
电调:淘宝 店家进口杂物爱好者 20-25A,2-3S,价格12-15元
电机:淘宝 店家进口杂物爱好者 1804无刷电机,价格14元
摇杆:买的坏遥控拆下来的,估价6元
软件部分:
定时器及PWM相关知识
ADC相关知识
SPI及串口相关知识
NRF24L01的相关函数使用
操作系统:FreeRTOS
开发环境:CubeMX+MDK5
开发语言:C语言
我在工程模板中定义了一个sys.h的头文件,主要是一些数据类型的重新命名及兼容F103和F407的代码,文件如下:
#ifndef _SYS_H
#define _SYS_H
// 适配不同芯片的移植
// 带FreeRTOS操作系统
// CubeMX+MDK开发环境
//-----------------------------------------------------------------------------------------------------------------
#ifdef STM32F103xE
#include "stm32f1xx_hal.h"
#endif
#ifdef STM32F407xx
#include "stm32f4xx_hal.h"
#endif
#include "stdint.h"
#include "FreeRTOS.h"
#include "cmsis_os.h"
#include "main.h"
//-----------------------------------------------------------------------------------------------------------------
//定义一些常用的数据类型短关键字
//-----------------------------------------------------------------------------------------------------------------
typedef int32_t s32;
typedef int16_t s16;
typedef int8_t s8;
typedef const int32_t sc32;
typedef const int16_t sc16;
typedef const int8_t sc8;
typedef __IO int32_t vs32;
typedef __IO int16_t vs16;
typedef __IO int8_t vs8;
typedef __I int32_t vsc32;
typedef __I int16_t vsc16;
typedef __I int8_t vsc8;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;
typedef const uint32_t uc32;
typedef const uint16_t uc16;
typedef const uint8_t uc8;
typedef __IO uint32_t vu32;
typedef __IO uint16_t vu16;
typedef __IO uint8_t vu8;
typedef __I uint32_t vuc32;
typedef __I uint16_t vuc16;
typedef __I uint8_t vuc8;
//-----------------------------------------------------------------------------------------------------------------
#endif
在单板测试之前都是使用F407核心板,在两者相互通信时,精英板为遥控方,F407核心板为接收方。
1. CubeMX搭建基于FreeRTOS工程模板
此部分参考我的“STM32之FreeRTOS学习笔记”中的1. CubeMX创建FreeRTOS工程模板,并且先可以新建跑马灯任务进行测试。注意:调试接口和时钟也要配置,这是基本操作,这里就不赘述了。
2. LED及按键配置
2.1 CubeMX配置
由于KEY_UP按下为高电平,因此初始状态让它下拉到低电平,而KEY_0按下为低电平,因此初始为上拉。LED低电平点亮,初始设置为高。
2.2 驱动函数及宏定义编写
经过上面的配置,我们就已经可以正常的使用IO口了,首先来编写LED相关头文件及源文件。
led.h如下:
#ifndef _LED_H
#define _LED_H
#define LED0_ON() HAL_GPIO_WritePin(LED0_GPIO_Port,LED0_Pin,GPIO_PIN_RESET)
#define LED0_OFF() HAL_GPIO_WritePin(LED0_GPIO_Port,LED0_Pin,GPIO_PIN_SET)
#define LED1_ON() HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET)
#define LED1_OFF() HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET)
#endif
无led.c
按键相关函数主要是移植正点原子的程序代码。
key.h如下:
#ifndef __KEY_H
#define __KEY_H
#include "sys.h"
#define KEY0_PRES 1 //KEY0 按下
#define KEY_UP_PRES 2 //KEY1 按下
#define KEY0 HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)
#define KEY_UP HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
u8 KEY_Scan(u8 mode); //按键扫描函数
#endif
key.c如下:
#include "key.h"
#include "led.h"
//按键处理函数
//返回按键值
//mode:0,不支持连续按;1,支持连续按;
//返回值:
//0,没有任何按键按下
//KEY0_PRES,KEY0按下
//KEY_UP_PRES,WK_UP按下
//注意此函数有响应优先级,KEY0>KEY1>WK_UP!!
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;//按键按松开标志
u8 key0=KEY0;
u8 keyup=KEY_UP;
if(mode)key_up=1; //支持连按
if(key_up&&(key0==0||keyup==1))
{
osDelay(5);
key_up=0;
if(key0==0)return KEY0_PRES;
else if(keyup==1)return KEY_UP_PRES;
}
else if(key0==1&&keyup==0)key_up=1;
return 0;// 无按键按下
}
//
//
// 按键测试代码
//-----------------------------------------------------------------------------------------------------------------
void KEY_Test()
{
u8 key=0;
static u8 tmp=0;
key=KEY_Scan(0);
switch(key)
{
case KEY0_PRES:
{
tmp=~tmp;
if(tmp)
{
LED0_ON();
}
else
{
LED0_OFF();
}
break;
}
case KEY_UP_PRES:
{
tmp=~tmp;
if(tmp)
{
LED1_ON();
}
else
{
LED1_OFF();
}
break;
}
}
}
//-----------------------------------------------------------------------------------------------------------------
经过上面的配置,就可以用两个按键分别点亮熄灭两个小灯了。
3. PWM配置
3.1 CubeMX配置
由于定时器 TIM1被用来给HAL库提供时钟基准,因此采用TIM2来配置PWM,配置过程如下:
第1步:点击TIM2
第2步:时钟选择内部
第3步:开启通道1到4
第4步:由于TIM2挂载在APB1上,时钟为84M,因此分频为8400,即一个时钟周期为100us,向上计数,重装载值200,即PWM周期为20ms,并使能重装载值。
第5步:设置PWM为模式1,即向上计数时,值cnt小于CCR1为有效电平,否则为无效电平。初始占空比设置为0,使能输出比较预装载,有效电平设置为高电平。
3.2 驱动函数及宏定义编写
pwm.h如下:
#ifndef _PWM_H
#define _PWM_H
// 代码移植及修改部分
// 适用于F407VGT6
// TIM2挂载在APB1总线上,时钟为84M
// 当前时钟分频为8400,即走一个时钟周期为100us
// 需要将PWM周期控制在20ms,因此重装载值要为200
// 控制占空比为1000-2000us,也就是10-20个时钟周期
//-----------------------------------------------------------------------------------------------------------------
#define definePWM_Small 10
#define definePWM_Big 20
//-----------------------------------------------------------------------------------------------------------------
void PWM_Tesk(void);
#endif
pwm.c文件如下:
#include "pwm.h"
#include "key.h"
#include "led.h"
extern TIM_HandleTypeDef htim2;
void PWM_Tesk()
{
u8 key=0,i=0;
static u8 cnt = definePWM_Small;
static u8 tmp=0;
key = KEY_Scan(0);
switch(key)
{
case KEY0_PRES:
{
tmp=~tmp;
if(tmp)
{
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,definePWM_Small);
LED0_OFF();
}
else
{
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,definePWM_Big);
LED0_ON();
}
break;
}
case KEY_UP_PRES:
{
cnt++;
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,cnt);
if(cnt>=definePWM_Big)
{
cnt=definePWM_Small;
for(i=0;i<5;i++)
{
LED1_ON();
osDelay(50);
LED1_OFF();
osDelay(50);
}
}
tmp=0;
break;
}
}
}
测试代码主要功能介绍:由于航模PWM控制模式都是采用周期为20ms,占空比1-2ms来实现控制的,本测试代码中,KEY0是用来设置最大最小占空比的,先按下KEY0为最小占空比,再按下则为最大占空比。相当于电机停止和电机满转功能,可用于电调的校准。KEY_UP则是按一下增加一次电机的转速,一共十个档位.(是根据TIM分频系数来决定的,此例程中十个时钟周期就是1ms,测试代码默认最低占空比为1ms,因此第10档就是2ms)最后面的tmp=0是为了按了KEY_UP之后,再去按KEY0的时候能保证第一次按下是电机停止的占空比,防止按完KEY_UP之后不下心误触KEY0电机直接满转对人造成伤害。KEY_UP里面的闪灯操作是为了提示电机速度已经最高,KEY0的亮灯也是提示目前是电机最高速度。注意:使用pwm的话,main函数在FreeRTOS系统调度之前要调用HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);启动PWM。第二个参数是指定pwm通道。
4. 摇杆按键程序设计
前面我们用按键代替摇杆控制了电机,但是按键始终是没有摇杆方便,因此我又买了一个航模摇杆,不要用那种蘑菇头的,手感极差,用航模遥控上面那种的。由于摇杆涉及到ADC,所以我们可通过CubeMX配置ADC。
4.1 CubeMX配置ADC
第一步:点击ADC1
第二步:设置通道,可勾选多个,我这里测试就只勾了一个
第三步:时钟分频系统会默认,不管他,也可以自己计算,STM32的ADC对时钟是有要求的。设置分辨率为12位,使能连续转换和DMA(注意:使能DMA要先进行第四步配置DMA)
第四步:配置DMA
先点击1处添加ADC1才会出现第二步的框框内容,点击第2个框框
设置为全字,即32位数据宽度。注意:右边的Mode选择circular
第五步:勾选ADC中断
ADC的配置就已经完毕了,为了便于调试,我们还可以开启串口,配置如下:
第二步模式选择异步即可。
因为要用到printf()函数,因此我们要进行串口重定向设置,下面这段代码就是,直接复制粘贴在一个头文件里,然后调用它即可,我是新建了usart1.c和.h文件,不要用usart.h,会和系统文件冲突。
usart1.h:
#ifndef _USART_H
#define _USART_H
#include "stdio.h"
#include "sys.h"
extern UART_HandleTypeDef huart1;
#endif
usart1.c:
#include "usart1.h"
// 串口重定向
// 实现printf输出
// 需要先配置串口
//----------------------------------------------------------------------------------------------------------------
int fputc(int ch, FILE *f) //轮询方式,超时机制,输出到串口函数重定义
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, sizeof(ch), 0xFFFF);
return ch;
}
/*HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)*/
int fgetc(FILE *f) //轮询方式,超时机制,接收到串口函数重定义
{
uint8_t ch;
HAL_UART_Receive(&huart1, (uint8_t *)&ch, sizeof(ch), 0xFFFF);
return ch;
}
//-----------------------------------------------------------------------------------------------------------------
经过以上的配置,我们就可以直接生成代码了,接下来就可以进行相关的测试代码编写及宏定义了。
4.2 驱动函数及宏定义编写
adc1.h如下:
#ifndef __ADC1_H
#define __ADC1_H
#include "sys.h"
extern UART_HandleTypeDef huart1;
extern uint32_t ADC_1;
//各采样30次,故30*2为60
extern uint32_t ADC_Value[30];
extern uint8_t i;
void ADC_DMA_Test(void);
#endif
adc1.c代码如下:
#include "adc1.h"
#include "usart1.h"
extern TIM_HandleTypeDef htim2;
uint32_t ADC_1;
//各采样30次,故30*2为60
uint32_t ADC_Value[30];
uint8_t i;
void ADC_DMA_Test(void)
{
uint8_t adc1;
//放个延迟,防止程序运行第一次读出数据有误
HAL_Delay(100);
ADC_1=0;
for(i=0,ADC_1=0;i<30;)
{
ADC_1+=ADC_Value[i++];
}
printf("ADC数据如下\n");
//除以30为求30次平均ADC值,乘以3.3为以3.3电压为基准,除以4096为ADC配置为12位
ADC_1/=30;
adc1=(uint8_t)(ADC_1/16);
adc1/=10;
adc1/=2;
adc1-=1;
adc1+=10;
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,adc1);
printf("ADC_IN0=%d\r\n",adc1);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
由于开启了ADC中断及DMA搬运,因此不需要CPU进行处理,我们只需要获取搬运后的的数据,将数据处理到10到20的范围内与占空比对应即可,然后赋值给调节占空比的函数。注意:使用ADC的话虽然设置了连续转换,但是刚开始在main函数处也需要先启动,调用HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC_Value,30);函数即可。
经过上面的配置,摇杆就可以成功控制点击的转速,摇杆杆位拨到最下方为停止,慢慢往上转速越来越高,最上方则达到最高转速。
5. NRF24L01程序设计
在单板上我们已经完成航模应该具有的基本功能了,包括舵机的控制其实是和电机一样的,因此我们现在就差无线通信了,无线通信涉及2方面的知识点,SPI通信和NRF24L01模块的使用
5.1 NRF24L01引脚介绍
一共8个引脚,电源引脚就不用说,供电3.3V。
SPI三根线:发送接收和时钟线
CSN:片选引脚,拉低此引脚则选中挂载在SPI总线上的器件
CE:NRF的传输使能引脚,写0不传输,写1开始传输,一般用在NRF写寄存器的时候,先失能,写寄存器,再使能。
IRQ:中断引脚,当发送完成NRF会将此引脚置0
5.2 Cube配置SPI
我们先配置遥控方(正点原子精英板)的SPI驱动,如图:
第二步:打开全双工
第三步:配置为主机数据单位为8bit,高位在前
第四步:时钟分频256,根据NRF的SPI特性配置为空闲状态SCL为低电平,第一个时钟边沿采样。
5.3 NRF24L01驱动及功能函数代码编写
nrf24l01.h:
#ifndef __24L01_H
#define __24L01_H
#include "sys.h"
//
//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,不循环;
//
//24L01操作线
#define NRF24L01_CE(ONorOFF) if(ONorOFF) \
HAL_GPIO_WritePin(NRF_CE_GPIO_Port,NRF_CE_Pin,GPIO_PIN_SET); \
else \
HAL_GPIO_WritePin(NRF_CE_GPIO_Port,NRF_CE_Pin,GPIO_PIN_RESET); //24L01片选信号
#define NRF24L01_CSN(ONorOFF) if(ONorOFF) \
HAL_GPIO_WritePin(NRF_CS_GPIO_Port,NRF_CS_Pin,GPIO_PIN_SET); \
else \
HAL_GPIO_WritePin(NRF_CS_GPIO_Port,NRF_CS_Pin,GPIO_PIN_RESET); //24L01片选信号 //SPI片选信号
#define NRF24L01_IRQ HAL_GPIO_ReadPin(NRF_IRQ_GPIO_Port,NRF_IRQ_Pin)
//24L01发送接收数据宽度定义
#define TX_ADR_WIDTH 5 //5字节的地址宽度
#define RX_ADR_WIDTH 5 //5字节的地址宽度
#define TX_PLOAD_WIDTH 32 //32字节的用户数据宽度
#define RX_PLOAD_WIDTH 32 //32字节的用户数据宽度
void nrf24l01_Test(void);
void NRF24L01_Init(void);//初始化
void NRF24L01_RX_Mode(void);//配置为接收模式
void NRF24L01_TX_Mode(void);//配置为发送模式
u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 u8s);//写数据区
u8 NRF24L01_Read_Buf(u8 reg, u8 *pBuf, u8 u8s);//读数据区
u8 NRF24L01_Read_Reg(u8 reg); //读寄存器
u8 NRF24L01_Write_Reg(u8 reg, u8 value);//写寄存器
u8 NRF24L01_Check(void);//检查24L01是否存在
u8 NRF24L01_TxPacket(u8 *txbuf);//发送一个包的数据
u8 NRF24L01_RxPacket(u8 *rxbuf);//接收一个包的数据
#endif
nrf24l01.c:
#include "spi2.h"
#include "nrf24l01.h"
#include "usart1.h"
#include "adc1.h"
//#include "led.h"
extern SPI_HandleTypeDef hspi2;
const u8 TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址
const u8 RX_ADDRESS[RX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址
//针对NRF24L01修改SPI1驱动
void NRF24L01_SPI_Init(void)
{
__HAL_SPI_DISABLE(&hspi2); //先关闭SPI2
hspi2.Init.CLKPolarity=SPI_POLARITY_LOW; //串行同步时钟的空闲状态为低电平
hspi2.Init.CLKPhase=SPI_PHASE_1EDGE; //串行同步时钟的第1个跳变沿(上升或下降)数据被采样
HAL_SPI_Init(&hspi2);
__HAL_SPI_ENABLE(&hspi2); //使能SPI2
}
//初始化24L01的IO口
void NRF24L01_Init(void)
{
// 下面这段只有正点原子精英板才需要
// GPIOB12初始化设置:推挽输出
// 正点原子的NRF和SPIFlash公用SPI2,为了不干扰NRF通信,故取消FLASH片选
//-----------------------------------------------------------------------------------------------------------------
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE(); //开启GPIOB时钟
//GPIOB12初始化设置:推挽输出
GPIO_Initure.Pin=GPIO_PIN_12; //PB12
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure); //初始化
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_12,GPIO_PIN_SET);//PB12输出1,防止SPI FLASH干扰NRF的通信
//-----------------------------------------------------------------------------------------------------------------
NRF24L01_SPI_Init(); //针对NRF的特点修改SPI的设置
NRF24L01_CE(0); //使能24L01
NRF24L01_CSN(1); //SPI片选取消
}
//检测24L01是否存在
//返回值:0,成功;1,失败
u8 NRF24L01_Check(void)
{
u8 buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5};
u8 i;
SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8); //spi速度为10.5Mhz((24L01的最大SPI时钟为10Mhz,这里大一点没关系)
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
}
//SPI写寄存器
//reg:指定寄存器地址
//value:写入的值
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); //返回状态值
}
//读取SPI寄存器值
//reg:要读的寄存器
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); //返回状态值
}
//在指定位置读出指定长度的数据
//reg:寄存器(位置)
//*pBuf:数据指针
//len:数据长度
//返回值,此次读到的状态寄存器值
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<len;u8_ctr++)pBuf[u8_ctr]=SPI2_ReadWriteByte(0XFF);//读出数据
NRF24L01_CSN(1); //关闭SPI传输
return status; //返回读到的状态值
}
//在指定位置写指定长度的数据
//reg:寄存器(位置)
//*pBuf:数据指针
//len:数据长度
//返回值,此次读到的状态寄存器值
u8 NRF24L01_Write_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<len; u8_ctr++)SPI2_ReadWriteByte(*pBuf++); //写入数据
NRF24L01_CSN(1); //关闭SPI传输
return status; //返回读到的状态值
}
//启动NRF24L01发送一次数据
//txbuf:待发送数据首地址
//返回值:发送完成状况
u8 NRF24L01_TxPacket(u8 *txbuf)
{
u8 sta;
SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8); //spi速度为6.75Mhz(24L01的最大SPI时钟为10Mhz)
NRF24L01_CE(0);
NRF24L01_Write_Buf(WR_TX_PLOAD,txbuf,TX_PLOAD_WIDTH);//写数据到TX BUF 32个字节
NRF24L01_CE(1); //启动发送
while(NRF24L01_IRQ==1); //等待发送完成
sta=NRF24L01_Read_Reg(STATUS); //读取状态寄存器的值
NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS,sta); //清除TX_DS或MAX_RT中断标志
if(sta&MAX_TX) //达到最大重发次数
{
NRF24L01_Write_Reg(FLUSH_TX,0xff); //清除TX FIFO寄存器
return MAX_TX;
}
if(sta&TX_OK) //发送完成
{
return TX_OK;
}
return 0xff;//其他原因发送失败
}
//启动NRF24L01接收一次数据
//txbuf:待发送数据首地址
//返回值:0,接收完成;其他,错误代码
u8 NRF24L01_RxPacket(u8 *rxbuf)
{
u8 sta;
SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8); //spi速度为6.75Mhz(24L01的最大SPI时钟为10Mhz)
sta=NRF24L01_Read_Reg(STATUS); //读取状态寄存器的值
NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS,sta); //清除TX_DS或MAX_RT中断标志
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到RX模式
//设置RX地址,写RX数据宽度,选择RF频道,波特率和LNA HCURR
//当CE变高后,即进入RX模式,并可以接收数据了
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为高,进入接收模式
}
//该函数初始化NRF24L01到TX模式
//设置TX地址,写TX数据宽度,设置RX自动应答的地址,填充TX发送数据,选择RF频道,波特率和LNA HCURR
//PWR_UP,CRC使能
//当CE变高后,即进入RX模式,并可以接收数据了
//CE为高大于10us,则启动发送.
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测试例程
void nrf24l01_Test(void)
{
}
由于正点原子的板子SPI Flash和NRF共用spi2,因此主函数处还要调用函数NRF24L01_Init()函数来配置Flash的片选引脚然后拉高,避免两个器件相互干扰。这个哈数还调用了一个NRF24L01_SPI_Init();这是正点原子为了让它的SPI配置兼容FLASH和NRF,因为两个器件的SPI的通信协议不同。如果你的SPI是我上面说的那样配置,那这句删掉即可。
现在我们来写例程,本来也是在nrf24l01.c最后面的测试例程写,后面发现通信不正常,所以直接写在了任务里面,之前的都是写的测试函数直接在任务中调用,任务函数如下:
void Fun_LED2(void *argument)
{
uint8_t adc1,i;
for(;;)
{
for(i=0,ADC_1=0;i<30;)
{
ADC_1+=ADC_Value[i++];
}
//除以30为求30次平均ADC值,乘以3.3为以3.3电压为基准,除以4096为ADC配置为12位
ADC_1/=30;
adc1=(uint8_t)(ADC_1/16);
adc1/=10;
adc1/=2;
adc1-=1;
adc1+=10;
printf("ADC_IN0=%d\r\n",adc1);
if(NRF24L01_TxPacket(&adc1)==TX_OK)
{
printf("发送完成");
}
osDelay(1);
}
}
这是直接用之前跑马灯的任务函数改了一下内容,所以任务名还是跑马灯的。
main函数进行任务调度之前可以检查一下NRF是否正常,代码如下:
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC_Value,30);
while(NRF24L01_Check())
{
printf("NRF Wait...");
}
printf("NRF OK");
NRF24L01_TX_Mode();
注意:一定要在mian函数进行任务调度之前打开ADC的DMA传输哦。
到这里基本就完成两者的通信了,什么?接收机的还没讲?不用讲了,通信这部分的nrf文件是一模一样的,只需要注意的是,遥控方只负责采集摇杆ADC发送出去,接收方只负责接收发送方的数据并且输出PWM即可。下面贴出接收机的任务函数和mian函数初始化部分。
void key0_Task(void *argument)
{
u8 i,nrfData;
/* USER CODE BEGIN key0_Task */
/* Infinite loop */
for(;;)
{
if(NRF24L01_RxPacket(&nrfData)==0)//一旦接收到信息,则执行下面代码提示用户,因为最小系统没集成串口,所以用LED来看状态.
{
__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,nrfData);
for(i=0;i<10;i++)
{
LED1_ON();
HAL_Delay(10);
LED1_OFF();
HAL_Delay(10);
}
// ADC_DMA_Test();
// PWM_Tesk();
osDelay(10);
}
}
/* USER CODE END key0_Task */
}
main函数任务调度之前调用下面这段
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
/* USER CODE BEGIN 2 */
while(NRF24L01_Check())
{
LED0_ON();
}
for(i=0;i<10;i++)
{
LED0_ON();
HAL_Delay(50);
LED0_OFF();
HAL_Delay(50);
}
NRF24L01_RX_Mode();
目前先写这么多,后面要做一个完整的遥控的话包括显示屏这些都得弄。