经过一个有意义的暑假,然而四级还是没过。参加了电子竞赛,发现自己真的需要好好地学习单片机,还需要更多硬件的知识!今年的比赛选择了B题,被风机坑了。虽然没有取得好的名次,但是参与了,收获还是多多的!今年第一次,没什么经验,自己加油,明年再战!
忙完了比赛,我还是好好继续来研究这个平衡车。上次更新到OLED的学习。有了显示,我们就可以进行更多的操作了。OLED的显示,能方便我们了解更多的内容!比如,采集的电压值,小车编码器的值都能清楚明了的显示在小车上,就不需要再接串口将数据传输到电脑上观察。
先来了解了解STM32F103C8T6的ADC。
STM32 的 ADC 是 12 位逐次逼近型的模拟数字转换器。它有18个通道,可测量16个外部和2个内部信号源,ADC_IN16为内部温度采集。各通道的 A/D 转换可以单次、连续、扫描或间断模式执行。 ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。
这个小车所使用的32芯片有2个ADC。STM32 的 ADC 最大的转换速率为 1Mhz,也就是转换时间为 1us(在 ADCCLK=14M,采样周期为1.5个 ADC 时钟下得到),不要让 ADC 的时钟超过 14M,否则将导致结果准确度下降。
根据原理图,
我们可以知道,对应的电压值采集的STM32引脚为PA4。所以采用ADC1的通道4。
那么在配置ADC的初始化时候,就需要配置GPIOA_4。
还是常规要求,在使用32的某个功能时,需要开启某部分的时钟,这里就需要开启ADC时钟,和GPIOA的时钟
第一步:使能GPIOA和ADC1通道时钟
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是方便小数部分的显示。
有了视频之后,接下来我们一起来学习STM32的定时器吧。
先来了解了解STM32F103C8T6的定时器,此芯片一共有四个定时器,其中一个高级定时器TIM1,3个通用定时器TIM2~TIM3,我主要学习的是通用定时器。高级定时器我自己现在学得还不是很明白,呵呵。通用定时器有四个独立通道。可以用来作为输入捕获、输出比较,PWM生成(边缘或中间对齐模式)、单脉冲模式输出。
这个小车四个定时器均有使用,其中使用了TIM2和TIM4的编码器,TIM3用于PWM波输出功能。定时器的溢出时间公式计算如下:
Tout=((arr+1)×(psc+1))/Tclk。
其中Tclk为TIM3的输入时钟频率,Tout为TIM3溢出时间
注意: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); } }
电机驱动实验
查看原理图了解驱动芯片及接口;
第一步。先了解驱动,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); }
编码器数据采集实验
首先了解编码器的硬件,此小车采用的是二手瑞士电机 具有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度安装的,为了保证前进后退时候的编码器数据符号一致
}
回复
有奖活动 | |
---|---|
【有奖活动】分享技术经验,兑换京东卡 | |
话不多说,快进群! | |
请大声喊出:我要开发板! | |
【有奖活动】EEPW网站征稿正在进行时,欢迎踊跃投稿啦 | |
奖!发布技术笔记,技术评测贴换取您心仪的礼品 | |
打赏了!打赏了!打赏了! |
打赏帖 | |
---|---|
【笔记】生成报错synthdesignERROR被打赏50分 | |
【STM32H7S78-DK评测】LTDC+DMA2D驱动RGBLCD屏幕被打赏50分 | |
【STM32H7S78-DK评测】Coremark基准测试被打赏50分 | |
【STM32H7S78-DK评测】浮点数计算性能测试被打赏50分 | |
【STM32H7S78-DK评测】Execute in place(XIP)模式学习笔记被打赏50分 | |
每周了解几个硬件知识+buckboost电路(五)被打赏10分 | |
【换取逻辑分析仪】RA8 PMU 模块功能寄存器功能说明被打赏20分 | |
野火启明6M5适配SPI被打赏20分 | |
NUCLEO-U083RC学习历程2-串口输出测试被打赏20分 | |
【笔记】STM32CUBEIDE的Noruletomaketarget编译问题被打赏50分 |