这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » w759067820的智能车DIY进程贴--!更新至小车绕8字!

共32条 2/4 1 2 3 4 跳转至
菜鸟
2015-09-02 21:26:37     打赏
11楼

经过一个有意义的暑假,然而四级还是没过。参加了电子竞赛,发现自己真的需要好好地学习单片机,还需要更多硬件的知识!今年的比赛选择了B题,被风机坑了。虽然没有取得好的名次,但是参与了,收获还是多多的!今年第一次,没什么经验,自己加油,明年再战!


菜鸟
2015-09-02 21:36:42     打赏
12楼

忙完了比赛,我还是好好继续来研究这个平衡车。上次更新到OLED的学习。有了显示,我们就可以进行更多的操作了。OLED的显示,能方便我们了解更多的内容!比如,采集的电压值,小车编码器的值都能清楚明了的显示在小车上,就不需要再接串口将数据传输到电脑上观察。

 

先来了解了解STM32F103C8T6ADC

STM32 ADC 12 位逐次逼近型的模拟数字转换器。它有18个通道,可测量16个外部和2个内部信号源,ADC_IN16为内部温度采集。各通道的 A/D 转换可以单次、连续、扫描或间断模式执行。 ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。

这个小车所使用的32芯片有2ADCSTM32 ADC 最大的转换速率为 1Mhz,也就是转换时间为 1us(在 ADCCLK=14M,采样周期为1.5 ADC 时钟下得到),不要让 ADC 的时钟超过 14M,否则将导致结果准确度下降。

根据原理图,

我们可以知道,对应的电压值采集的STM32引脚为PA4。所以采用ADC1的通道4

那么在配置ADC的初始化时候,就需要配置GPIOA_4

还是常规要求,在使用32的某个功能时,需要开启某部分的时钟,这里就需要开启ADC时钟,和GPIOA的时钟

第一步:使能GPIOAADC1通道时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1 , ENABLE ); 接下来初始化ADC引脚。

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //输入模拟信号:模式为模拟输入

GPIO_Init(GPIOA,&GPIO_InitStructure);

第二步;复位ADC1,设置分频因子

由于使用的库函数,需要将

stm32f10x_adc.c这个文件添加进工程,

接下来就可以直接调用函数

ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值

RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12, ADC最大时间不能超过14M

第三步,初始化ADC1参数

ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //独立工作模式

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并校准

在完成上面的操作就可以使能和校准ADC。为了得到精准的AD转换结果,都必须要校准ADC。但是要注意的是,每次进行校准之后要等待校准结束。这里是通过获取校准状态来判断是否校准是否结束。

下面是ADC使能和校准的方法:

ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1

ADC_ResetCalibration(ADC1); //使能复位校准

while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束

ADC_StartCalibration(ADC1); //开启AD校准

while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束

第五步:读取AD转换值

上面校准完成之后,就可以设置规则序列 1 里面的通道,采样顺序,以及通道的采样周期,然后启动 ADC 转换。在转换结束后,读取 ADC 转换结果值就是了。

设置规则序列通道以及采样周期的函数:

ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,序列1,采样时间为239.5周期

设置软件开启 ADC 转换的方法是:

ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能

我们要判断 ADC1的转换是否结束。所以需要 进行以下操作:

while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC )); //等待转换结束

最后再获取ADC转换结果

ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果。

这里可以将获取ADC值得程序编写为一个子程序

/*************************************************************************

函数功能:获得ADC

入口参数:ADC1 的通道

返回 值:AD转换结果

*************************************************************************/

u16 Get_Adc(u8 ch)

{

ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 );

ADC_SoftwareStartConvCmd(ADC1, ENABLE);

while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));

return ADC_GetConversionValue(ADC1);

}

在实际的程序中,一般都是多次采集AD值,然后取平均值,这样能得到较为准确的值。

所以我编写下面的函数来实现

/*************************************************************************

函数功能:获得多次ADC平均值

入口参数:ADC1 的通道,采样次数

返回 值:AD转换结果

*************************************************************************/

u16 Get_Adc_Average(u8 ch,u8 times)

{

u32 temp_val=0;

u8 t;

for(t=0;t

{

temp_val+=Get_Adc(ch);

delay_ms(5);

}

return temp_val/times;

}

通过这种方式获取ADC的值为0-4096;我们还需要进一步转换,才能得到电压值。由于STM32没有可设置的外部电压,所以使用内部3.3V为参考电压,需要采集的电压值大于3.3v,所以在硬件设计将设置AD采样电阻,查看原理图,可以看到,最终AD采集的电压为R2上面的电压。

下面是读取电压值的子程序

/*************************************************************************

函数功能:读取电池电压

入口参数:无

返回 值:无

*************************************************************************/

void Get_battery_volt(void)

{

Voltage=Get_Adc(4)*3.3*11.5*100/1.5/4096;

}

这里多乘了100是方便小数部分的显示。


助工
2015-09-03 09:22:38     打赏
13楼
好详细 

菜鸟
2015-09-11 09:40:39     打赏
14楼
代码可以专门用代码编辑器哦~

菜鸟
2015-09-17 18:56:57     打赏
15楼
更新节点!终于有咯视频!

菜鸟
2015-09-25 15:58:39     打赏
16楼

有了视频之后,接下来我们一起来学习STM32的定时器吧。

 

先来了解了解STM32F103C8T6定时器,此芯片一共有四个定时器,其中一个高级定时器TIM13个通用定时器TIM2~TIM3,我主要学习的是通用定时器。高级定时器我自己现在学得还不是很明白,呵呵。通用定时器有四个独立通道。可以用来作为输入捕获、输出比较,PWM生成(边缘或中间对齐模式)、单脉冲模式输出。

这个小车四个定时器均有使用,其中使用了TIM2TIM4的编码器,TIM3用于PWM波输出功能。定时器的溢出时间公式计算如下:

                     Tout=((arr+1)×(psc+1))/Tclk

其中TclkTIM3的输入时钟频率,ToutTIM3溢出时间

注意:STM32的通用定时器是挂在APB1上的,但是时钟频率是72MHz,是APB1的两倍

 

接下来以TIM2的定时器中断为例介绍一下定时器的使用过程

第一步:TIM2时钟使能

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

 

第二步:初始化定时器参数

定时器的参数全在下面这个结构体里面

typedef struct

{

  uint16_t TIM_Prescaler;                                              

  uint16_t TIM_CounterMode;      

  uint16_t TIM_Period;          

  uint16_t TIM_ClockDivision;   

  uint8_t TIM_RepetitionCounter; 

} TIM_TimeBaseInitTypeDef;

第一个参数是用来设置分频系数的。

第二个参数是用来设置计数方式的,有向上计数、向下计数和中央对齐计数方式,比较常用的是向上计数模式。

第三个参数是设置自动重载计数周期值

第四个参数是用来设置时钟分频因子的

第五个参数只有使用高级定时器时才使用,这里就先不做介绍。

 

第三步:设置TIM2的允许更新中断,在库函数中是通过TIM_ITConfig函数来实现的

TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

 

第四步:进行TIM2的中断优先级设置

在定时器中断使能之后因为要产生中断,必不可少地要设置NVIC相关寄存器及中断优先级。

关于中断优先级的讲解,下面这个链接讲的还是比较清楚

http://www.amobbs.com/thread-5540241-1-1.html

 

第五步:使能定时器TIM2

光配置好定时器还不能使用,不要忘了还要开启它哦

TIM_Cmd(TIM2, ENABLE);

 

第六步:编写中断服务函数

定时器溢出中断后,我们就可以在中断服务函数干自己想干的事啦,哈哈

接下来还是贴上我自己的代码吧

void TIM2_Init(u16 arr, u16 psc) //arr:自动重装值。psc:时钟预分频数
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	TIM_TimeBaseStructure.TIM_Period = arr;
	TIM_TimeBaseStructure.TIM_Prescaler = psc;
	TIM_TimeBaseStructure.TIM_ClockDivision  = 0;
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
	//TIM_TimeBaseStructure.TIM_RepetitionCounter = 高级定时器才用,通用定时器不用
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_Cmd(TIM2, ENABLE);
}


这就是定时器参数的配置代码,在附上中断函数的代码吧

void TIM2_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
	{
		LED1 =! LED1;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

 


 


菜鸟
2015-09-25 15:59:49     打赏
17楼
说得好

菜鸟
2015-10-21 17:55:38     打赏
18楼

电机驱动实验

查看原理图了解驱动芯片及接口;



第一步。先了解驱动,TB6612FNG是一款直流电机的驱动芯片。两路输入信号可以控制电机的正转反转刹车和停止。

允许输入最大的电压为15V

最重要的就是了解功能驱动表:



第二步。了解PWM定时器。两轮平衡车有两个电机,需要两路PWM波驱动,根据原理图,可以看出,小车的电机的驱动连接的是 PB0 PB 1 两个端口,通过查找STM32数据手册知道用的TIM3的通道3和通道4


附上重要的程序:

首先需要定义电机正反转控制端口

#define AIN2   PBout(15)
#define AIN1   PBout(14)
#define BIN1   PBout(13)
#define BIN2   PBout(12)

这样的宏定义方便以后直接用AN1 AN2 .。简洁明了

PWM控制

/**************************************************************************
函数功能:PWM 以及电机控制的IO初始化
入口参数:arr:自动重装值  psc:时钟预分频数 
返回  值:无
**************************************************************************/
void PWM_Init(u16 arr,u16 psc)
{
	GPIO_InitTypeDef	GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	TIM_OCInitTypeDef	TIM_OCInitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);   //使能 GPIOB 时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);    //使能 TIM3 时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;   //设置PWM输出 IO 口 PB0 PB1
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;        //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;      //IO速度50MHz
	GPIO_Init(GPIOB,&GPIO_InitStructure);				   //根据设定值初始化
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15;  //设置电机驱动控制IO PB12 PB13 PB14 PB15
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  								//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;								//最高输出速率 50MHz	
	GPIO_Init(GPIOB, &GPIO_InitStructure);											//根据设定值初始化
	
	TIM_TimeBaseStructure.TIM_Period = arr;						 //重装值
	TIM_TimeBaseStructure.TIM_Prescaler = psc;					 //预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;                 //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);				 //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
	
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;			  //TIM脉冲宽度调制 PWM模式1
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;                            //设置待装入捕获比较寄存器的脉冲值
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;     //输出极性:TIM输出比较极性高

	TIM_OC3Init(TIM3, &TIM_OCInitStructure);           //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
	TIM_OC4Init(TIM3, &TIM_OCInitStructure);           //根据TIM_OCInitStruct中指定的参数初始化外设TIMx

	
	TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);  //CH3预装载使能	 
	TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);  //CH4预装载使能
	
	TIM_ARRPreloadConfig(TIM3, ENABLE);                //使能TIM3在ARR上的预装载寄存器
	
	TIM_Cmd(TIM3, ENABLE);                             //使能TIM3
	TIM_SetCompare4(TIM3,200);
	TIM_SetCompare3(TIM3,200);
}


		

TB6612驱动电机:

/**************************************************************************
函数功能:PWM驱动电机转动
入口参数:左轮PWM、右轮PWM
**************************************************************************/
void Set_Pwm(int moto1,int moto2)
{
			if(moto1<0) AIN2=1, AIN1=0; else AIN2=0, AIN1=1; TIM_SetCompare4(TIM3,moto1); if(moto2<0) BIN1=0, BIN2=1; else BIN1=1, BIN2=0; TIM_SetCompare3(TIM3,moto2); }




菜鸟
2015-10-23 09:37:31     打赏
19楼

可以在平衡车基础上加点自己小创意~



菜鸟
2015-10-23 17:15:41     打赏
20楼

编码器数据采集实验

首先了解编码器的硬件,此小车采用的是二手瑞士电机 具有512线的正交编码器 ,指的是电机转一圈,产生512正交脉冲信号。增量式正交编码器, 它产生两个方波信号 A B 它们相差+/- 90°, 其符号由转动方向决定。

STM32F10x 的所有通用定时器及高级定时器都集成了正交编码器接口。定时器的两个输入 TI1 TI2 直接与增量式正交编码器接口。

选择编码器接口模式的方法是:如果计数器只在 TI2 的边沿计数,则置 TIM1_SMCR 寄存器中的 SMS=001;如果只在 TI1 边沿计数,则置 SMS=010 如果计数器同时在 TI1 TI2 边沿计数,则置 SMS=011

32的编码器标准模式为X4. 1000线的编码器每转一周可发出4000个计数脉冲

在编写程序之前,先看原理图。以编码器1为例。接口为PA0和PA1,对应的是STM32的TIM2 CH1和CH2。

所以我们需要配置定时器2的编码器模式。由于使用的是库函数,所以必须了解的是编码器配置函数 TIM_EncoderInterfaceConfig





下面将我的程序附上


编码器初始化程序

/**************************************************************************
函数功能:编码器1初始化
入口参数:无
返回  值:无
**************************************************************************/
void Encoder1_Init(void)
{
	TIM_ICInitTypeDef  TIM2_ICInitStructure;
	GPIO_InitTypeDef	GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能PB 6 7时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);  //使能TIM4 时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;    //设置PWM输出 IO 口 PB6 PB7
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;   //浮空输入
	GPIO_Init(GPIOA,&GPIO_InitStructure);				    //根据设定值初始化
	

	TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD-1;       //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	 计数到5000为500ms
	TIM_TimeBaseStructure.TIM_Prescaler = 0;                       //设置用来作为TIMx时钟频率除数的预分频值 7200得到 10Khz的计数频率  
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up|TIM_CounterMode_Down;  //TIM中央对齐模式3
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;        //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);                //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位

	TIM_ITConfig(TIM2, TIM_IT_Update ,ENABLE);  //使能//TIM1//使能或者失能指定的TIM中断

	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;  //TIM4中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  //先占优先级1级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级3级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
	
//TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Falling|TIM_ICPolarity_Rising,TIM_ICPolarity_Falling|TIM_ICPolarity_Rising);
	TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
	
	
  	TIM2_ICInitStructure.TIM_ICFilter = 0x03;           //IC1F=0011 配置输入滤波器 fSAMPLING=fCK_INT, N=8
  	TIM_ICInit(TIM2, &TIM2_ICInitStructure);
	
	TIM_SetCounter(TIM2, COUNTER_RESET);
		
	TIM_Cmd(TIM2,ENABLE);
}

 

在进行编码器读取的时候一定要考虑中断,不然读取编码器的值不会清零。


读取数据程序

/**************************************************************************
函数功能:读取编码器的数据并进行数据类型转换
入口参数:无
返回  值:无
**************************************************************************/

void ReadEncoder(void)
{
	  u16 Encoder_L,Encoder_R;       //===左右编码器的脉冲计数
		Encoder_R = TIM4 -> CNT;       //===获取正交解码1数据	
		TIM4 -> CNT=0;                 //===计数器清零  
	  Encoder_L= TIM2 -> CNT;        //===获取正交解码2数据	
	  TIM2 -> CNT=0;	               //===计数器清零
		if(Encoder_L>32768)  Encoder_Left=Encoder_L-65000; else  Encoder_Left=Encoder_L;  
	  //=这个处理的原因是:编码器到0后会跳到65000向下计数,这样处理方便我们在控制程序中使用
	  if(Encoder_R>32768)  Encoder_Right=Encoder_R-65000; else  Encoder_Right=Encoder_R;
	 	Encoder_Left=-Encoder_Left;//这里取反是因为,平衡小车的两个电机是旋转了180度安装的,为了保证前进后退时候的编码器数据符号一致
}

 


 


共32条 2/4 1 2 3 4 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]