目录
目录
模块设计
本次设计使用到的模块有:12锂电池1;12V转3.3V和5V电源模块;两个NRF24L01模块;两个STM32F103C8T6;PS2手柄摇杆(带回中);两个直流电机;TB6612电机驱动模块
- TB6612电机驱动模块详细介绍:
- PS2手柄摇杆(带回中)详细介绍:
后续更新-----
一、具体的接线图
1、无线遥控器端
(1)NRF24L01与STM32接线
NRF24L01引脚图:
IRQ -------- PB11 CE -------- PB12 CSN-------- PB10
SCK -------- PB13 MOSI -------- PB15 MISO -------- PB14
(2)PS2手柄摇杆与STM32接线
控制前进和后退的摇杆:
VRy ----- PA2 GND ----- GND +5V ----- 5V
控制左转和右转的摇杆:
VRx ----- PA6 GND ----- GND +5V ----- 5V
其余引脚可不接,看你需求接。
2、小车端
(1)NRF24L01与STM32接线
IRQ -------- PB1 CE -------- PB0 CSN-------- PA4
SCK -------- PA5 MOSI -------- PA7 MISO -------- PA6
(2)TB6612与STM32接线
VM -------- 5V VCC -------- 3.3V GND-------- GND STBY ----- 5V
AIN1 -------- PA0 AIN2 -------- PA1 PWMA -------- PA2
BIN1 -------- PB14 BIN2 -------- PB15 PWMB -------- PA3
(3)TB6612与直流电机接线
AO1和AO2任意接两根电机线,BO1和BO2同理。
二、主控的选择
本次设计采用STM32F103系列芯片,STM32F103C8T6是一款基于ARM Cortex-M 内核STM32系列的32位的微控制器,程序存储器容量是64KB,需要电压2V~3.6V,工作温度为-40°C ~ 85°C。
三、怎么实现无线遥控?
结合上一篇SPI通信的博文,本次设计采用的是SPI通信的方式实现无线遥控的功能。SPI具体的介绍,请看基于STM32的SPI通信和IIC通信协议学习笔记-CSDN博客
本次设计采用NRF24L01模块实现的无线遥控功能,下图是模块的示意图:
实现的具体思路:使用一个STM32F103系统板连接NRF24L01,作为发送端(即无线遥控器);另外使用一个 STM32F103系统板连接NRF24L01,作为接收端(小车)。通过发送数据给小车,实现小车前进,后退,左转,右转等功能。
1、发送端
通过ADC的采样,采集PS2手柄摇杆模块的模拟值,进行一系列处理后,使用SPI通信发送数据给接收端。
2、接收端
通过对接收到的数据进行计算处理成合适的速度值,将处理好的速度值放进电机驱动函数里面,实现电机的正转和反转,从而实现小车的基本功能。
四、无线遥控模块程序的编写
1、NRF24L01程序编写
打开keil5界面,添加以下三个文件:
(1)配置NRF24L01寄存器地址
引脚仅供参考,可以自行修改
NRF24L01_Ins.h 文件:
#ifndef __nRF24L01_H
#define __nRF24L01_H
/********** NRF24L01引脚定义 ***********/
#define IRQ_Port GPIOB
#define CE_Port GPIOB
#define CSN_Port GPIOB
#define SCK_Port GPIOB
#define MOSI_Port GPIOB
#define MISO_Port GPIOB
#define IRQ_Pin GPIO_Pin_11
#define CE_Pin GPIO_Pin_12
#define CSN_Pin GPIO_Pin_10
#define SCK_Pin GPIO_Pin_13
#define MOSI_Pin GPIO_Pin_15
#define MISO_Pin GPIO_Pin_14
/********** NRF24L01寄存器操作命令 ***********/
#define nRF_READ_REG 0x00
#define nRF_WRITE_REG 0x20
#define RD_RX_PLOAD 0x61
#define WR_TX_PLOAD 0xA0
#define FLUSH_TX 0xE1
#define FLUSH_RX 0xE2
#define REUSE_TX_PL 0xE3
#define NOP 0xFF
/********** NRF24L01寄存器地址 *************/
#define CONFIG 0x00
#define EN_AA 0x01
#define EN_RXADDR 0x02
#define SETUP_AW 0x03
#define SETUP_RETR 0x04
#define RF_CH 0x05
#define RF_SETUP 0x06
#define STATUS 0x07
#define OBSERVE_TX 0x08
#define CD 0x09
#define RX_ADDR_P0 0x0A
#define RX_ADDR_P1 0x0B
#define RX_ADDR_P2 0x0C
#define RX_ADDR_P3 0x0D
#define RX_ADDR_P4 0x0E
#define RX_ADDR_P5 0x0F
#define TX_ADDR 0x10
#define RX_PW_P0 0x11
#define RX_PW_P1 0x12
#define RX_PW_P2 0x13
#define RX_PW_P3 0x14
#define RX_PW_P4 0x15
#define RX_PW_P5 0x16
#define FIFO_STATUS 0x17
/****** STATUS寄存器bit位定义 *******/
#define MAX_TX 0x10 //达到最大发送次数中断
#define TX_OK 0x20 //TX发送完成中断
#define RX_OK 0x40 //接收到数据中断
#endif
(2)NRF24L01 模块初始化配置
-
设置地址宽度和有效数据宽度:定义发送和接收地址的宽度分别为 5 字节,有效数据宽度为 32 字节。
-
定义地址值:定义发送和接收地址的数值,用于与目标设备通信。
-
实现改变引脚电平的函数:
- W_SS(uint8_t BitValue):用于改变 SS 引脚的输出电平,操作 CSN 引脚。
- W_CE(uint8_t BitValue):用于改变 CE 引脚的输出电平,用于控制模块的工作状态。
- W_SCK(uint8_t BitValue):用于改变 SCK 引脚的输出电平,用于 SPI 通信时的时钟信号。
- W_MOSI(uint8_t BitValue):用于改变 MOSI 引脚的输出电平,用于 SPI 通信的数据输出。
- R_MISO(void):用于读取 MISO 引脚的输入电平,用于 SPI 通信的数据输入。
- R_IRQ(void):用于读取 IRQ 引脚的输入电平,用于处理中断信号。
NRF24L01.c 文件第一部分代码:
# include "NRF24L01.h"
#include "NRF24L01_Ins.h"
#include "stm32f10x.h" // Device header
#include "Delay.h"
#define TX_ADR_WIDTH 5 //5字节地址宽度
#define RX_ADR_WIDTH 5 //5字节地址宽度
#define TX_PLOAD_WIDTH 32 //32字节有效数据宽度
#define RX_PLOAD_WIDTH 32 //32字节有效数据宽度
const uint8_t TX_ADDRESS[TX_ADR_WIDTH]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const uint8_t RX_ADDRESS[RX_ADR_WIDTH]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
//改变SS引脚的输出电平
void W_SS(uint8_t BitValue)
{
GPIO_WriteBit(CSN_Port, CSN_Pin, (BitAction)BitValue);
}
//改变CE引脚的输出电平
void W_CE(uint8_t BitValue)
{
GPIO_WriteBit(CE_Port, CE_Pin, (BitAction)BitValue);
}
//改变SCK引脚的输出电平
void W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(SCK_Port, SCK_Pin, (BitAction)BitValue);
}
//改变MOSI引脚的输出电平
void W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(MOSI_Port, MOSI_Pin,(BitAction)BitValue);
}
//读取MISO引脚的输出电平
uint8_t R_MISO(void)
{
return GPIO_ReadInputDataBit(MISO_Port, MISO_Pin);
}
//读取IRQ引脚的输出电平
uint8_t R_IRQ(void)
{
return GPIO_ReadInputDataBit(IRQ_Port, IRQ_Pin);
}
NRF24L01.c 文件第二部分代码:
//初始化NRF24L01使用的引脚
//其中CSN、SCK、MOSI、CE设置为推挽输出 IRQ、MISO设置为上拉输入模式
void NRF24L01_Pin_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = CSN_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(CSN_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = SCK_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SCK_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = MOSI_Pin;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MOSI_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = CE_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(CE_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = MISO_Pin;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(MISO_Port, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = IRQ_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(IRQ_Port, &GPIO_InitStructure);
}
//标准SPI交换一个字节
uint8_t SPI_SwapByte(uint8_t Byte)
{
uint8_t i;
for(i = 0; i < 8; i ++)
{
if((uint8_t)(Byte & 0x80) == 0x80)
{
W_MOSI(1);
}
else
{
W_MOSI(0);
}
Byte = (Byte << 1);
W_SCK(1); //模拟时钟上升沿
Byte |= R_MISO(); //接收从机发来的一个数据
W_SCK(0); //模拟时钟下降沿
}
return Byte;//交换从机的一个完整字节并返回
}
//通过调用SPI与NRF24L01交换一个字节
//Reg:交换的第一个字节就是从机的寄存器地址
//Value:交换的第二个字节就是要在这个寄存器写入的数据
uint8_t NRF24L01_Write_Reg(uint8_t Reg, uint8_t Value)
{
uint8_t Status;
W_SS(0);
Status = SPI_SwapByte(Reg);
SPI_SwapByte(Value);
W_SS(1);
return Status;
}
//通过调用SPI读取NRF24L01的一个字节
//Reg:交换的第一个字节就是要读取的从机的寄存器地址
//Value:再次调用SPI的交换函数,用NOP交换会寄存器的储存的数据并用Value接收并返回
uint8_t NRF24L01_Read_Reg(uint8_t Reg)
{
uint8_t Value;
W_SS(0);
SPI_SwapByte(Reg);
Value = SPI_SwapByte(NOP);
W_SS(1);
return Value;
}
//在指定的寄存器写入一堆数据,和上面函数的关系上面的函数只写一个,这个可以写很多个
//Reg:要读取的寄存器地址
//*Buf:要写入寄存器的的数据的首地址
//Len:要写入的数据长度
uint8_t NRF24L01_Write_Buf(uint8_t Reg, uint8_t *Buf, uint8_t Len)
{
uint8_t Status, i;
W_SS(0);
Status = SPI_SwapByte(Reg);
for(i = 0; i < Len; i ++)
{
SPI_SwapByte(*Buf ++);
}
W_SS(1);
return Status;
}
//在指定的寄存器读取一堆数据,和上面函数的区别就是上面的读一个这个能读很多个
//Reg:要读取的寄存器地址
//*Buf:用于存储读取到的数据的数组的首地址
//Len:要读取的数据的长度
uint8_t NRF24L01_Read_Buf(uint8_t Reg, uint8_t *Buf, uint8_t Len)
{
uint8_t Status, i;
W_SS(0); //下拉SS表示选中从机
Status =SPI_SwapByte(Reg);
for(i = 0; i < Len; i ++)
{
Buf[i] = SPI_SwapByte(NOP);
}
W_SS(1); //拉高SS表示通讯结束
return Status; //返回状态值作用不大
}
//通过调用函数向WR_TX_PLOAD寄存器写入要发送的数据(在WR_TX_PLOAD寄存器中写入的数据就是要发送的数据)
//*Buf:要写入寄存器发射的数据的数组的首地址,也就是要发送的数据,最多可以发送32个字节
uint8_t NRF24L01_SendTxBuf(uint8_t *Buf)
{
uint8_t State;
W_CE(0); //CE置0调为发射模式
NRF24L01_Write_Buf(WR_TX_PLOAD, Buf, TX_PLOAD_WIDTH);
W_CE(1); //CE置1发射
while(R_IRQ() == 1); //检测中断位
State = NRF24L01_Read_Reg(STATUS);
NRF24L01_Write_Reg(nRF_WRITE_REG + STATUS, State); //清除中断位
if(State&MAX_TX) //判断是否发射达到最大次数触发中断
{
NRF24L01_Write_Reg(FLUSH_TX, NOP);
return MAX_TX;
}
if(State & TX_OK) //判断是否发射成功触发中断
{
return TX_OK;
}
return NOP; //其他情况返回NOP
}
//通过调用函数读取RD_RX_PLOAD寄存器读取接收到的数据(在RD_RX_PLOAD寄存器中储存的就是接收到的数据)
//*Buf:储存读取到的数据的首地址
//和上面的函数相比没有CE的设置,因为默认状态下CE为高电平,就是默认的接收模式
uint8_t NRF24L01_GetRxBuf(uint8_t *Buf)
{
uint8_t State;
State = NRF24L01_Read_Reg(STATUS); //读取STATUS状态寄存器判读触发的中断类型
NRF24L01_Write_Reg(nRF_WRITE_REG + STATUS, State); //再次在STATUS寄存器的位置写入1可以重置中断
if(State & RX_OK)
{
W_CE(1); //为了确保是接收模式,假设不写也没关系,但是写了更保险
NRF24L01_Read_Buf(RD_RX_PLOAD, Buf, RX_PLOAD_WIDTH);
NRF24L01_Write_Reg(FLUSH_RX, NOP);
W_CE(1);
Delay_us(150); //适当延迟以延长接收时间
return 0;
}
return 1;
}
//用于检测
uint8_t NRF24L01_Check(void)
{
uint8_t check_in_buf[5] = {0x11 ,0x22, 0x33, 0x44, 0x55};
uint8_t check_out_buf[5] = {0x00};
//保证SCK、SS、CE被初始化
W_SCK(0);
W_SS(1);
W_CE(0);
//将check_in_buf数组中的数据写入TX_ADDR发送地址寄存器中
NRF24L01_Write_Buf(nRF_WRITE_REG + TX_ADDR, check_in_buf, 5);
//读取TX_ADDR寄存器里的数据并储存在check_out_buf中
NRF24L01_Read_Buf(nRF_READ_REG + TX_ADDR, check_out_buf, 5);
//鉴定check_out_buf中储存的数据和写入的check_in_buf中的一样,如果一样说明NRF24L01已经连接
if((check_out_buf[0] == 0x11) && (check_out_buf[1] == 0x22) && (check_out_buf[2] == 0x33) && (check_out_buf[3] == 0x44) && (check_out_buf[4] == 0x55))
{
return 0; //如果检测成功返回0
}
else
{
return 1; //检测失败返回1
}
}
//初始化NRF24L01的发送或者接收状态
void NRF24L01_RT_Init(void)
{
W_CE(0); //以下可能要进行接收或者发射模式的转换,模式转换时CE引脚要先置0
//初始化设置接收数据的数据宽度(为32字节)
NRF24L01_Write_Reg(nRF_WRITE_REG+RX_PW_P0, RX_PLOAD_WIDTH);
//调用FLUSH_RX指令可以清空接收缓存区,发送的NOP没用
NRF24L01_Write_Reg(FLUSH_RX, NOP);
//写入TX_ADDR寄存器的是发送的数据的接收地址,TX_ADR_WIDTH为固定的地址长度(长度为5个字节)
NRF24L01_Write_Buf(nRF_WRITE_REG + TX_ADDR, (uint8_t*)TX_ADDRESS, TX_ADR_WIDTH);
//写入RX_ADDR_P0确认第一个接收地址(最多可以有6个),RX_ADR_WIDTH为固定的地址长度(长度为5个字节)
NRF24L01_Write_Buf(nRF_WRITE_REG + RX_ADDR_P0, (uint8_t*)RX_ADDRESS, RX_ADR_WIDTH);
//启动自动应答
NRF24L01_Write_Reg(nRF_WRITE_REG + EN_AA, 0x01);
//使能数据接收的通道,这里只使用通道0,因此在EN_RXADDR寄存器中写入0000 0001
NRF24L01_Write_Reg(nRF_WRITE_REG + EN_RXADDR, 0x01);
//配置SETUP_RETR寄存器 0001 1010 高四位表示延时槽,0001:表示两个延时槽(250×2=86=586us) 1010:表示重发10次
NRF24L01_Write_Reg(nRF_WRITE_REG + SETUP_RETR, 0x1A);
//工作频率=(2400 + RF_CH) MHz,在这个寄存器写0表示工作频率为2.4G
NRF24L01_Write_Reg(nRF_WRITE_REG + RF_CH, 0);
//配置寄存器0000 1111 配置只有接收到对应地址触发中断,默认进入接收模式,且发射结束后自动进入接收模式接收应答信号
//B东西无脑配成0x0F就行
//NRF24L01_Write_Reg(nRF_WRITE_REG + RF_SETUP, 0x0F);
//CONFIG寄存器就这个样配置(默认配置为接收模式)
NRF24L01_Write_Reg(nRF_WRITE_REG + CONFIG, 0x0F);
W_CE(1);
}
//初始化
//引脚初始化,检查,NRF24L01寄存器初始化
void NRF24L01_Init()
{
NRF24L01_Pin_Init();
while(NRF24L01_Check());
NRF24L01_RT_Init();
}
//
void NRF24L01_SendBuf(uint8_t *Buf)
{
W_CE(0); //模式转换
//CONFIG最低位置0,配置为发射模式
NRF24L01_Write_Reg(nRF_WRITE_REG + CONFIG, 0x0E);
W_CE(1);
Delay_us(15);//15us之后进入发射模式
NRF24L01_SendTxBuf(Buf);//将数据写入寄存器中发射
W_CE(0);//模式转换
NRF24L01_Write_Reg(nRF_WRITE_REG + CONFIG, 0x0F);
W_CE(1);
}
//看看NRF24L01是不是触发中断
uint8_t NRF24L01_Get_Value_Flag()
{
return R_IRQ();
}
NRF24L01.h 文件代码:
#ifndef __nRF24L01_API_H
#define __nRF24L01_API_H
#include "stm32f10x.h" // Device header
uint8_t SPI_SwapByte(uint8_t byte);
uint8_t NRF24L01_Write_Reg(uint8_t reg,uint8_t value);
uint8_t NRF24L01_Read_Reg(uint8_t reg);
uint8_t NRF24L01_Read_Buf(uint8_t reg,uint8_t *pBuf, uint8_t len);
uint8_t NRF24L01_Write_Buf(uint8_t reg, uint8_t *pBuf, uint8_t len);
uint8_t NRF24L01_GetRxBuf(uint8_t *rxbuf);
uint8_t NRF24L01_SendTxBuf(uint8_t *txbuf);
uint8_t NRF24L01_Check(void);
void NRF24L01_RT_Init(void);
void NRF24L01_Init(void);
void NRF24L01_SendBuf(uint8_t *Buf);
void NRF24L01_Pin_Init(void);
uint8_t NRF24L01_Get_Value_Flag();
#endif
到这里,无线遥控器的NRF24L01的程序已经写完了,接下来是ADC函数的配置。
2、ADC采集PS2手柄遥控模拟值
ADC函数的详细配置请看博客:STM32 ADC转换器、串口输出_stm32 adf输出-CSDN博客
后续我会补充这里。
以下是使用到的引脚
/***********************************
ADC1_CH2---PA2 左摇杆左右 ----> ADC1的通道2
ADC1_CH1---PA1 左摇杆上下 ----> ADC1的通道1
ADC1_CH3---PA3 右摇杆左右 ----> ADC1的通道3
ADC1_CH6---PA6 右摇杆上下 ----> ADC1的通道6
ADC1_CH4---PA4 电源检测 ----> ADC1的通道4
************************************/
关于电压采集这部分请看: STM32电压采集电路设计-CSDN博客
补充:ADC函数的配置
创建两个文件 ADC.c 和 ADC.h
(1)ADC1的初始化
- 编写ADC初始化函数,建立ADC 和 GPIO结构体,通过打开以下头文件
头文件拉到底,找到ADC初始化的函数
ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);
- 配置相应的时钟,ADC1和GPIOA的时钟 ,配置ADC分配因子
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
- 配置使用到的引脚,注意是GPIO模式配置为 模拟输入
// 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
- 接下来是ADC结构体参数的配置,还有ADC使能,复位校准和开启校准的设置
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
完整初始化程序:
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
// 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
}
(2)获取ADC各通道的值
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
(3) 对采集到ADC值均值滤波
u16 Get_Adc_Average(u8 ch,u8 times) //对采集的数据进行均值滤波
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
Delay_ms(1);
}
return temp_val/times;
}
(4)采集函数封装
void BAT_Value(void) //电源电压 对应引脚PA4
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_0,5);
power_Value =(float)Value*3.3/4096;
}
void Remote_Channel1_Value(void) //左遥感控制上下的通道 对应引脚PA1
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_1,5);
channel1_Value = Value;
}
void Remote_Channel2_Value(void) //左遥感控制左右的通道 对应引脚PA2
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_2,5);
channel2_Value = (float)Value;
}
void Remote_Channel6_Value(void) //右遥感控制上下的通道 对应引脚PA6
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_6,5);
channel6_Value = (float)Value;
}
void Remote_Channel3_Value(void) //右遥感控制左右的通道 对应引脚PA3
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_3,5);
channel3_Value = (float)Value;
}
(4)完整的ADC.c 和 ADC .h:
ADC.c
#include "adc.h"
#include "delay.h"
/***********************************
ADC1_CH2---PA2 左摇杆左右
ADC1_CH1---PA1 左摇杆上下
ADC1_CH3---PA3 右摇杆左右
ADC1_CH6---PA6 右摇杆上下
ADC1_CH4---PA4 电源检测
************************************/
//GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6;
float power_Value,channel1_Value,channel2_Value,channel6_Value,channel3_Value;
//channel
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
// 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
}
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
u16 Get_Adc_Average(u8 ch,u8 times) //对采集的数据进行均值滤波
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
Delay_ms(1);
}
return temp_val/times;
}
void BAT_Value(void) //电源电压 对应引脚PA4
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_0,5);
power_Value =(float)Value*3.3/4096;
}
void Remote_Channel1_Value(void) //左遥感控制上下的通道 对应引脚PA1
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_1,5);
channel1_Value = Value;
}
void Remote_Channel2_Value(void) //左遥感控制左右的通道 对应引脚PA2
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_2,5);
channel2_Value = (float)Value;
}
void Remote_Channel6_Value(void) //右遥感控制上下的通道 对应引脚PA6
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_6,5);
channel6_Value = (float)Value;
}
void Remote_Channel3_Value(void) //右遥感控制左右的通道 对应引脚PA3
{
u16 Value;
Value=Get_Adc_Average(ADC_Channel_3,5);
channel3_Value = (float)Value;
}
ADC.h
#ifndef _adc_h_
#define _adc_h_
#include "stm32f10x.h"
void Adc_Init(void);
void BAT_Value(void); //电源电压
void Remote_Channel1_Value(void);
void Remote_Channel2_Value(void);
void Remote_Channel6_Value(void);
void Remote_Channel3_Value(void);
#endif
主代码修改为以下形式:
extern float power_Value,channel1_Value,channel2_Value,channel6_Value,channel3_Value;
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
OLED_Clear();
Adc_Init();
while (1)
{
Remote_Channel1_Value();
Remote_Channel2_Value();
Remote_Channel6_Value();
Remote_Channel3_Value();
OLED_ShowNum(0,2,channel1_Value,4,16);
OLED_ShowNum(0,4,channel2_Value,4,16);
OLED_ShowNum(64,2,channel6_Value,4,16);
OLED_ShowNum(64,4,channel3_Value,4,16);
}
}
3、主程序的编写(旧)
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "AD.h"
#include "NRF24L01.h"
uint8_t Speed = 0x00;
uint8_t Direction = 0x00;
uint8_t Control[32];
int main(void)
{
/*模块初始化*/
AD_Init(); //AD初始化
NRF24L01_Init(); //NRF24L01初始化
while (1)
{
//定义8位的速度和方向,用于控制电机和舵机
//要发送的信息只有两个
Control[0]=2;
//用数组接收速度和方向的参数用于发送
Control[1]= Speed = (uint8_t)(AD_Value[1]>>4);
Control[2]=Direction=(uint8_t)(AD_Value[2]>>4);
NRF24L01_SendBuf(Control);
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
将模拟输入的 AD 值进行处理,然后将处理后的数值存储到 Control 数组中。
//用数组接收速度和方向的参数用于发送
Control[1]= Speed = (uint8_t)(AD_Value[1]>>4);
Control[2]=Direction=(uint8_t)(AD_Value[2]>>4);
- (uint8_t)(AD_Value[1]>>4)`:通过右移 4 位操作,将 AD_Value[1] 的值向右移动 4 位,并转换为 8 位无符号整数(uint8_t)。然后将结果存储到 Control[1] 变量中,这个值用来表示速度 Speed。
- (uint8_t)(AD_Value[2]>>4)`:通过右移 4 位操作,将 AD_Value[2] 的值向右移动 4 位,并转换为 8 位无符号整数(uint8_t)。然后将结果存储到 Control[2] 变量中,这个值用来表示方向 Direction。
为什么获取到的模拟值要做移位处理呢? 我们来分析一下:
假设我们有一个模拟输入传感器,其输出范围是 0 到 1023(10 位精度)。我们想要将这个范围内的数值转换为一个更适合用于控制系统的范围,比如 0 到 255(8 位精度)。这时候可以利用右移位操作来处理这些数值。
例如,假设传感器输出的数值为 682。在代码中进行右移 4 位(即 AD_Value >> 4
)后,682 变成了 42。这个处理后的数值 42 可以更好地映射到控制系统所需的范围内(0 到 255),方便后续控制逻辑的处理。学到了哈哈哈。
五、小车程序的编写
1、NRF24L01程序编写
NRF24L01.c文件和 NRF24L01_Ins.h 文件 的配置 与无线遥控器的程序一样,这里不做多记录了。
唯一改变的是NRF24L01.h中的引脚定义部分改为以下引脚:
/********** NRF24L01引脚定义 ***********/
#define IRQ_Port GPIOB
#define CE_Port GPIOB
#define CSN_Port GPIOA
#define SCK_Port GPIOA
#define MOSI_Port GPIOA
#define MISO_Port GPIOA
#define IRQ_Pin GPIO_Pin_1
#define CE_Pin GPIO_Pin_0
#define CSN_Pin GPIO_Pin_4
#define SCK_Pin GPIO_Pin_5
#define MOSI_Pin GPIO_Pin_7
#define MISO_Pin GPIO_Pin_6
2、PWN输出函数的编写
定时器的具体配置流程请参考:
pwm.c 文件:
#include "stm32f10x.h" // Device header
/**
* 函 数:PWM初始化
* 参 数:使用 PA2-TIM2_CH3 & PA3-TIM2_CH4
* 返 回 值:无
*/
void PWM_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA2引脚初始化为复用推挽输出
//受外设控制的引脚,均需要配置为复用模式
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*输出比较初始化*/
TIM_OCInitTypeDef TIM_OCInitStructure; //定义结构体变量
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化,若结构体没有完整赋值
//则最好执行此函数,给结构体所有成员都赋一个默认值
//避免结构体初值不确定的问题
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式,选择PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性,选择为高,若选择极性为低,则输出高低电平取反
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
TIM_OCInitStructure.TIM_Pulse = 0; //初始的CCR值
TIM_OC3Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC3Init,配置TIM2的输出比较通道3
TIM_OC4Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC4Init,配置TIM2的输出比较通道4
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/**
* 函 数:PWM设置CCR
* 参 数:Compare 要写入的CCR的值,范围:0~100
* 返 回 值:无
* 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
* 占空比Duty = CCR / (ARR + 1)
*/
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare); //设置CCR3的值
}
void PWM_SetCompare4(uint16_t Compare)
{
TIM_SetCompare4(TIM2, Compare); //设置CCR4的值
}
头文件:这里就不写出来了,只需把相关函数放进去就好了。
3、电机控制函数的编写
通过操作PS2手柄摇杆 ,接收端获取发送端传输的数据后,进行一系列的处理操作,将其转换成合适的速度值,放进Motor_go_SetSpeed(int8_t Speed)和Motor_LR_SetSpeed(int8_t Direction)函数中,实现控制引脚的高低电平和PWM占空比的设置,实现小车的基本功能。
Motor.c 文件:
#include "stm32f10x.h" // Device header
#include "PWM.h"
/**
* 函 数:左路直流电机初始化
* 参 数:无
* 返 回 值:无
*/
void MotorL_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4和PA5引脚初始化为推挽输出
PWM_Init(); //初始化直流电机的底层PWM
}
/**
* 函 数:右路直流电机初始化
* 参 数:无
* 返 回 值:无
*/
void MotorR_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB14和PB15引脚初始化为推挽输出
PWM_Init(); //初始化直流电机的底层PWM
}
/**
* 函 数:直流电机设置速度
* 参 数:Speed 要设置的速度,范围:-100~100
* 返 回 值:无
*/
void Motor_go_SetSpeed(int8_t Speed)
{
if (Speed >= 127) //如果设置正转的速度值
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); //PA4置高电平
GPIO_SetBits(GPIOA, GPIO_Pin_1); //PA5置低电平,设置方向为正转
PWM_SetCompare3(Speed); //PWM设置为速度值
GPIO_ResetBits(GPIOB, GPIO_Pin_14); //PA4置高电平
GPIO_SetBits(GPIOB, GPIO_Pin_15); //PA5置低电平,设置方向为正转
PWM_SetCompare4(Speed); //PWM设置为速度值
}
else if(Speed < 127) //否则,即设置反转的速度值
{
GPIO_SetBits(GPIOA, GPIO_Pin_0); //PA4置低电平
GPIO_ResetBits(GPIOA, GPIO_Pin_1); //PA5置高电平,设置方向为反转
PWM_SetCompare3(-Speed); //PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
GPIO_SetBits(GPIOB, GPIO_Pin_14); //PA4置低电平
GPIO_ResetBits(GPIOB, GPIO_Pin_15); //PA5置高电平,设置方向为反转
PWM_SetCompare4(-Speed); //PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
}
}
void Motor_LR_SetSpeed(int8_t Direction)
{
if (Direction >= 57) //如果设置左转的速度值
{
GPIO_ResetBits(GPIOB, GPIO_Pin_14); //PA4置高电平
GPIO_SetBits(GPIOB, GPIO_Pin_15); //PA5置低电平,设置方向为正转
PWM_SetCompare4(Direction); //PWM设置为速度值
}
else if(Direction < -57) //否则,即设置反转的速度值
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); //PA4置低电平
GPIO_SetBits(GPIOA, GPIO_Pin_1); //PA5置高电平,设置方向为反转
PWM_SetCompare3(Direction); //PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
}
}
4、主程序的编写
main.c 文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "NRF24L01.h"
#include "MOTOR.h"
int8_t Speed; //定义速度变量
int8_t Direction;
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
NRF24L01_Init(); //NRF24L01的初始化
MotorL_Init(); //左路两个电机
MotorR_Init(); //右路两个电机
uint8_t Buf[32] = {0}; //定义用于接收NRF24L01数据的数组
OLED_ShowString(1, 6, "Len:"); //OLED显示
OLED_ShowString(2, 6, "Y:");
OLED_ShowString(3, 6, "X:");
while (1)
{
if (NRF24L01_Get_Value_Flag() == 0)
{
NRF24L01_GetRxBuf(Buf);
}
Speed=Buf[1] * 20 / 26 - 100;
Direction=Buf[2] * 20 / 26 - 100;
// OLED_ShowNum(1, 10, Buf[0], 2);
OLED_ShowSignedNum(2, 8, Speed, 3);
OLED_ShowSignedNum(3, 8, Direction, 3);
if (Speed)
{
Motor_go_SetSpeed(Speed); //小车的前进和后退
}
if(Direction)
{
Motor_LR_SetSpeed(Direction); //小车的左转和右转
}
}
}
待完善---