(纯萌新,学习单片机半年了,这是寒假回家的作品,师从江科大,写博客纪录我实现后的经验)(比较粗略)
前言:
因为没买超声波避障模块,只能勉强一下用红外寻迹做一个类似的小车。
问题很多:光线会干扰读取到的AD值,且难以设置详细的距离,只能设置大概距离;碍于萌新我不会PID算法的I算法的编程,以及暂时无法读取电机的转苏,做不出内环,只能勉强做一个只有P,D的PID跟随小车,但实现基本功能是绰绰有余的;本文只用了一个模块,所以只能直线,如果要做到转弯等功能,可以三个模块再用简单的if语句就像寻迹小车一样即可。
目标:
- 能够使用单片机读取红外模块的AD值
- 写简单的没有I的PID算法,实现比较稳定的简单跟踪
- 写main函数结合上述目标
接下来分析做该项目的思路:
首先要解决读取红外模块的数值,红外模块有四根接口,其中AO是读取具体电压值的口,DO是只输出0/1的口。所以接AO。用AD的代码即可读取其值。当其遇见不反光的面时,会输出很高的AD值,反之为低。使用AD的原因是如果用DO口,只能识别有无,但是没法控制距离。
其次是PID的算法,萌新我实力有限,只能写没有I的PID算法博君一笑了。P指的是跟随比例变化电机的转速,D指的是能接收的最终误差。具体可以看其他大佬的PID讲解,很详细,但是我暂时没能将其完全写成自己的代码。
最后就是通过PID算法获知应该输入给电机的转速了,大概思路就是如此。
解决&源码:
首先是AD读取,这里建议观看江科大老师的stm32教程,很容易上手的代码(比较重要的部分在注释中已经标明。ps.标注释真是个好习惯)。
#include "stm32f10x.h" // Device header
#include "AD.h"
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); //打开ADC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //打开ADCCLK的6分频
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //专属ADC的模式捏
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//结构体初始化ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode= ADC_Mode_Independent; //ADC1单打模式!
ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right ; //右对齐就欧克啦
ADC_InitStructure.ADC_ExternalTrigConv= ADC_ExternalTrigConv_None; //只用软件触发啦
ADC_InitStructure.ADC_ContinuousConvMode=DISABLE ; //连续转换模式(ENABLE)
ADC_InitStructure.ADC_NbrOfChannel= 1; //通道数目
ADC_InitStructure.ADC_ScanConvMode= DISABLE; //扫描模式 看江科大啦
ADC_Init(ADC1,&ADC_InitStructure);
ADC_Cmd(ADC1,ENABLE); //ADC准备就绪捏
//但是我们要校准呢
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET); //没校准开始给我站在这里
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET); //等待校准完成捏
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}
uint16_t AD_GetValue_D(void)
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE); //连续触发可放在上面,这样也不需要等到转化完成啦↓
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET); //转换完成变成1就退出啦
return ADC_GetConversionValue(ADC1); //因为读取后自动清除标志位,所以不用清除标志位啦
}
//若是连续触发模式,可以将软件触发的函数挪到初始化的最后,只需要触发一次即可~
uint16_t AD_GetValue_E(void) //这个就是连续转换用的函数啦
{
return ADC_GetConversionValue(ADC1);
}
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
ADC_SoftwareStartConvCmd(ADC1,ENABLE); //连续触发可放在上面,这样也不需要等到转化完成啦↓
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET); //转换完成变成1就退出啦
return ADC_GetConversionValue(ADC1); //因为读取后自动清除标志位,所以不用清除标志位啦
}
然后是PID
#include "stm32f10x.h" // Device header
#include "AD.h"
void PID_Init(void)
{
AD_Init();
}
float PID(uint16_t Aim,uint16_t D) //Aim是目标,D是误差
{
uint16_t Now;
float Speed=0;
Now=AD_GetValue(ADC_Channel_8); //假如3500是目标,50是误差,则可以计算具体比例
if(Now<(Aim-D))
{
Speed=((Now-Aim)/70)-50;
return(Speed); //返回的速度
}
else if(Now>(Aim+D))
{
Speed=((Now-Aim)/7)+30;
return(Speed);
}
else {return 0;}
}
最后是main代码
#include "stm32f10x.h"
#include "Delay.h"
#include "Motor.h"
#include "PID.h"
#include "OLED.h"
#include "AD.h" // Device header
int main(void) //电机范围-100~100
{
float Speed;
Motor_Init();
PID_Init();
OLED_Init();
while(1)
{
Speed=PID(3500,75);
Motor_L_SetSpeed(Speed);
Motor_R_SetSpeed(Speed);
OLED_ShowNum(1,1,Speed+100,3);
OLED_ShowNum(2,1,AD_GetValue(ADC_Channel_8),4);
}
}
尾声:
本次作品的实现,让我提前适应了超声波避障的思路,便于之后的继续学习和研究。自己编写极其简单的PID算法也让我知道了许多算法的精妙之处,适应算法的难度。这是学习路上一个避不开的坎,努力坚持下去,终会有成果的。