欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[2011 年全国大学生电子设计竞赛] 基于 STM32 的帆板控制系统的保姆级教学

最编程 2024-05-21 08:34:27
...

前言:

最近笔者在学长的建议下开始研究往年电子设计竞赛的赛题。其中的一项赛题(帆板控制系统)非常适合入门选手(指笔者本人)上手实操,于是经过几天的研究,笔者与自己小组也成功制作出了一套符合该题的解决方案,特在此记录,一方面加深自己的印象,另一方面方便他人模仿学习,共同进步。

一、题目要求:

QQ截图20220406195324.png

二、器件准备:

本次实验共用到以下材料:

1.工业型铝材20x20×18和配套铝角件、螺钉、螺母若干,用于搭建整体框架

0AFFCC0F5A8620B3587E6248B7C25032.jpg

2.锌合金带座轴承立式×1(孔径约10mm),用于固定转动轴:

QQ图片20220406202355.jpg

3.精密导电塑料电位器×1(总阻值约1KΩ),用于测量转动角度

170F426B01252D5BCF18DA966ECEDF18.jpg

4.涡轮鼓风机×1(12V 1.6A),用于提供风力稳定帆板(建议风机功率选择大一些,否则无法吹动帆板到指定角度)

D4DF0B211F93D6C55D83ADF76CA56116.jpg

5.D19L25绕线联轴器×1(外径约14mm),用于锁紧转动轴与电位器

B7AE49E03C0679AB11C3E3507340CB99.jpg

6.L298N电机驱动模块×1,用于控制涡轮鼓风机(应注意L298N的OUT口输出电流应小于L298N可承受的电流峰值,否则容易烧毁电机驱动模块)

02ADECFA79779C9F91F7526BE812299F.jpg

7.转动轴×1(φ0.9*300)

02C31A1D0C2176CC6B0CC9D5CDDECC1B.jpg

8.锂电池(12V)×1用于给风机供电(最好再自己制作一个开关)

QQ图片20220406211954.jpg

9.自制硬纸板(10cm×15cm)×1代替帆板

三、实现原理:

①机械结构方面:

首先采用工业铝型材20x20搭建“工”字形底座,再如图连接铝材,形成相互连接的双“T”字形框架。

B5566502C18D83667FC0F1752BC69889.jpg

再在与地面垂直的两条铝材上分别安装滚动轴承与电位器,其中电位器用于测量转动角度(电位器的固定装置为团队内部自主设计,详情设计方案请看附件),而轴承起支撑转动轴与减小转动阻力的作用。我们再按照题目要求剪切10cm x 15cm的硬纸板,通过透明胶带与转轴连接作为帆板。再在固定风机的杆上固定好鼓风机(此处暂且用铝角件固定),两根水平杆之间的距离按照赛题要求间隔10cm。

3BDCBFB24431E5479A3F8CBA2EE8C651.jpg

再将图中电位器的1,2,3,引脚分别连接至单片机VCC引脚,单片机的ADC采样口(即对应ADC外设的采样通道所在引脚),单片机GND引脚。如此一来,单片机对电位器采样的硬件连接便完成了。

QQ图片20220406225153.jpg

接下来如图将L298N电机驱动模块与风机和单片机对应PWM输出引脚进行连接,具体连接方式见下图:

QQ图片20220406225153.jpg

完成上述全部安装和接线步骤后,插上12V电池,那么在硬件方面的配置就宣告完成!

E7D8DB10F476DDE0EF368AF51E22BE06.jpg

②软件实现方面:

首先分析通过电位器测量帆板实时角度的原理。由于我们本次采用的是旋转式电位器,该电位器的特点是能够将旋转角度的变化量线性转换为对应的电阻值的变化,进而影响到电压的变化。如此一来,我们便可以大胆猜测角度与电位器该点电压的关系也应该是呈线性关系。那么基于这个猜想,我们可以先让帆板垂直悬挂,并在这一时刻通过STM32单片机自带的ADC外设去读取该时刻的电压值V0,且将该电压值所对应的角度值记录为0°;再将帆板扶正至90°,通过ADC读取这时的电压值V1,并将该电压值对应的角度记录为90°。通过数学运算,我们不难得出Angle=k×Voltage+b这样的一条一次关系式,其中k=(90-0)/(V1-V0),b=Angle-k×Voltage。那么出于严谨性的考量,我们依然需要对该式进行验证,这里笔者已经经过实际测量,可以确定该式是符合电压与帆板转过角度的关系的,读者有兴趣可以自行尝试,此处便不再赘述。

本次例程中的ADC模块代码

/***********************************************************
*@brief:	ADC引脚配置
***********************************************************/
static void ADC1_GPIOConfig(void)
{
	GPIO_InitTypeDef GPIO_InitTypeStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	GPIO_InitTypeStructure.GPIO_Mode=GPIO_Mode_AIN;        //设置为模拟输入
	GPIO_InitTypeStructure.GPIO_Pin=GPIO_Pin_1;
	GPIO_InitTypeStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOC,&GPIO_InitTypeStructure);
}

/***********************************************************
*@brief:	ADC模式配置
***********************************************************/
static void ADC1_ModeConfig(void)
{
	ADC_InitTypeDef	ADC_InitTypeStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	ADC_InitTypeStructure.ADC_ContinuousConvMode=DISABLE;    //不进行连续转换
	ADC_InitTypeStructure.ADC_DataAlign=ADC_DataAlign_Right; //禁止触发检测,使用软件触发
	ADC_InitTypeStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
	ADC_InitTypeStructure.ADC_Mode=ADC_Mode_Independent;     //独立模式
	ADC_InitTypeStructure.ADC_NbrOfChannel=1;                //只转换规则序列1
	ADC_InitTypeStructure.ADC_ScanConvMode=DISABLE;          //非扫描模式
	ADC_Init(ADC1,&ADC_InitTypeStructure);

	RCC_ADCCLKConfig(RCC_PCLK2_Div8);             //设置分频因子6 即12M,ADC最大时间不能超过14M
	ADC_RegularChannelConfig(ADC1,ADC_Channel_11,1,ADC_SampleTime_55Cycles5);

	ADC_ITConfig(ADC1,ADC_IT_EOC,ENABLE);         //开启ADC转换完成中断
	ADC_Cmd(ADC1,ENABLE);
	//开始复位校准ADC1
	ADC_ResetCalibration(ADC1);
	//等待ADC1的复位校准完毕,校准完毕会产生复位(RESET)信号
	while(ADC_GetResetCalibrationStatus(ADC1));
	//开始校准ADC1
	ADC_StartCalibration(ADC1);
	//等待ADC1校准完毕,校准完毕会产生复位(RESET)信号
	while(ADC_GetCalibrationStatus(ADC1));
}

/***********************************************************
*@brief:	ADC初始化函数
***********************************************************/
void ADC1_Init(void)
{
	ADC1_GPIOConfig();        //配置输入引脚
	ADC1_ModeConfig();        //配置ADC模式
}

/***********************************************************
*@brief:	ADC获取转换值
*@param:	None
*@retval:	转换后的电压值
***********************************************************/
uint16_t ADC1_GetConvertValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	
	//查询ADC转换状态寄存器,等待转换完成
	while(!ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC));
	return ADC_GetConversionValue(ADC1);
}

下图为实际测量时实际帆板角度与通过ADC采样后计算得出的角度值的对比,可以看出电压与角度的线性关系符合得非常完美(LCD具体显示代码已经包含在附件的工程文件中,各位读者可自行下载研究):

E163AAA43F54D523EE86DEC7FC7E2882.jpg

接下来便是要通过PWM来控制风机转速,吹动帆板到达预设角度。本例程中我们将定时器3用作PWM生成定时器,初始化代码如下,笔者根据资料查阅与学长的意见,将定时器3的欲分频系数由原先的1分频改为720分频,这样的频率设置对电机来说较为合适。

QQ截图20220407195422.png

接下来便是对风机速度的控制了,但在该过程中我们仍需考虑其它因素对帆板的干扰(如电池的电压降低了、转动轴与联轴器之间存在摩擦力,环境风对帆板的扰动等等),为此,我们需要引入PID闭环控制来实时监控帆板角度并调节。

至于什么是PID控制,在此笔者推荐一篇****博主的文章,对PID原理分析较好: 一文读懂PID控制算法(抛弃公式,从原理上真正理解PID控制)

理解了何为PID闭环控制,我们再试着将PID的数学原理转化为C语言,并嵌入实际代码中。以下是笔者给出的增量式PID控制算法模板:

struct PID                          //定义pid结构体用来记录pid各参量
{
	float Now_err;
	float Err_k2;
	float Err_k1;
	float kp;
	float ki;
	float kd;
	float Sv;
}pid;

/***********************************************************
*@brief:	增量式PID实现算法,通过该函数获得每一次的增量
*@param:	angle:		当前的角度值
*@retval:	ans:		所得解
*@note:
*		pid.kp:	比例系数,应在本函数运行前被指定
*		pid.ki:	积分系数,应在本函数运行前被指定
*		pid.kd:	微分系数,应在本函数运行前被指定
*		pid.Now_err:	第k次误差
*		pid.Err_k1:     k-1次误差
*		pid.Err_k2:	k-2次误差
***********************************************************/
float Pid_Realize(float angle)
{
        float ans;

        pid.Now_err=pid.Sv-angle;                            //得到当前角度与设定角度的误差值
        ans=pid.kp*(pid.Now_err-pid.Err_k1)+pid.ki*pid.Now_err
        +pid.kd*(pid.Now_err-2*pid.Err_k1+pid.Err_k2);
        pid.Err_k2=pid.Err_k1;
        pid.Err_k1=pid.Now_err;
        return ans;
}


Speed+=Pid_Realize(Angle);                  //不断累加每一次Pid_Realize()函数返回的增量(ans)

TIM_SetCompare3(TIM3,Speed);                //所得值Speed便为下一次调节风机所需的PWM占空比

得到了上述的增量式PID算法模板之后,我们便可以将其应用到本次的PWM控制风机转速上,而对于Kp,Ki,Kd三个参量,本次例程只用到了Kp与Ki。其中Kp作为比例系数,用于快速缩小当前角度与预设角度的误差值,但由于理想模型与实际之间存在着无法避免的反应时间,故过大的Kp控制可能会导致风机“超调”的现象(帆板被过度吹高,后慢慢回到预设角度;或帆板在预设角度附近较大范围不停震荡,无法稳定),此时我们减小比例系数Kp,增加积分系数Ki,然而,过大的Ki依然可能导致超调现象,为此我们需要尽可能地协调Kp与Ki,像这样再经过一段时间耐心调试,我们的帆板应该就能够平稳地维持在预设角度值,并且有着比较好的抗干扰性能(主要表现在用手随意拨动帆板之后,帆板依然能够快速回到预设位置)以及在改变预设角度后能够快速到达预期值。值得一提的是,当前角度过大于预设角度时,PID计算函数的返回值便会变为负数,而在STM32定时器外设

TIM_SetCompareX(TIM_TypeDef* TIMx, uint16_t CompareX);

函数中,CompareX的参数类型为uint16_t,追踪定义至源代码可以发现其实质为unsigned short int类型,故小于零的参数传入会导致数据类型出错(实际表现为当帆板角度过大时,风机的风速不断从最大变到0,角度越大频率越快)。为此我们还须对输入的参数做限制:

          Speed+=Pid_Realize(Angle); //不断累加每一次Pid_Realize()函数返回的增量(ans)
		if(speed>=1000)			//最大上限限制
		{
		    speed=1000;
		}
		if(speed<=10)			//最小下限限制
		{
		    speed=10;
		}
                TIM_SetCompare3(TIM3,Speed);    //所得值Speed便为下一次调节风机所需的PWM占空比

为了将液晶屏的刷新参数显示与PWM更新独立分离开来,以提高整个系统的稳定性,我们还需要将PWM的更新语句放到定时器5的中断服务函数中去,这样就得到了读者在实际工程文件中所看到的代码如下:

/***********************************************************
*@brief:	定时器5中断服务函数
***********************************************************/
void TIM5_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM5,TIM_IT_Update)==SET)	//检查中断标志位
	{
		speed+=Pid_Realize(Angle);	//将计算出的调节值叠加到speed上
		if(speed>=1000)			//最大上限限制
		{
			speed=1000;
		}
		if(speed<=10)		        //最小下限限制
		{
			speed=10;
		}
		TIM_SetCompare3(TIM3,speed);	//通过speed调节PWM,形成闭环
		TIM_ClearITPendingBit(TIM5,TIM_IT_Update);	//清空中断标志位
	}
}

笔者搭建的帆板系统在实际测试时,能够在开机2秒之内到达初始预设角度,且控制准确度在+-1°以内,整体符合了赛题要求

四、附录:

1.笔者团队成员设计的电阻固定装置

QQ图片20220407142623.png2.本次例程所用到的源代码,这里一并附上百度网盘链接

链接:pan.baidu.com/s/1C9Jms4_Y… 提取码:iga3