这道题总的来说是比较简单的,在这里只做到控制帆板到指定位置。
怎么样通过对风扇转速的控制,来调节风力大小,来改变帆板转角。也就是说给你一个角度值,你用风扇吹到那个角度值。控制风力大小,这个就需要控制PWM的大小了。如果我们知道吹到哪个角度值,所需要的PWM是多大,不就可以了吗?

结果非常尴尬,60度的PWM试不出来,这个是因为装置的结构决定,导致它在这个角度非常不稳定,不好试出来这个PWM。

这个时候应该怎么办?这个就需要PID调节了,这个相当于,PID帮我们试出来这个PWM应该是多大。
我们需要了解什么是PID
这里我推荐一个视频,外国的视频,目前我看过最好的,非常清晰,B站上的。
我们知道什么是PID了,他的公式是什么呢。
下面推荐看一下这个CSDN的一个博主讲的,非常不错,了解公式是怎么来的。
我们现在把他的公式变成代码


Kp、Ki、Kd都是PID的系数。
红色相当于第一部分的比例,现在的误差
黄色相当于第二部分的积分,误差的累积。
蓝色相当于第三部分的微分,现在的误差减去上一个误差。
绿色相当于我们的公式了,就是把他们加起来。
非常好理解,这个是绝对式PID,我们现在看一下,另一种—增量式PID


Kp、Ki、Kd都是PID的系数。
红色相当于第一部分的比例,现在的误差减去上一个误差。
黄色相当于第二部分的积分,现在的误差。误差的累积。
蓝色相当于第三部分的微分,现在的误差减去两倍上一个误差,加上上个别的误差。
绿色相当于我们的公式了,就是把他们加起来。
我们从公式上看,增量式PID只需要现在的误差,上次的,上上次的,没有像绝对式那样误差的积分,个人觉得增量是比较合适的,发现绝对式不太好调参数,我觉得都可以试一下吧。

现在看一下我们的PID执行函数
红色是我们的当前值和目标值。
黄色是PID系数。
蓝色是要执行的PID算法。
绿色就把计算的PID值输出。
这样子的话,我们接下来需要做的是采集当前角度值和做好PWM的输出。
怎么采集角度值呢?
使用角度传感器啊

角度传感器,个人感觉非常准,只需要STM32AD读取的时候再滤一下波就好了。
下面是AD转化配置
/*******************************************************************************
* 函 数 名 : ADCx_Init
* 函数功能 : ADC初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ADCx_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;//ADC
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN; //模拟输入
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
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_InitStructure.ADC_NbrOfChannel = 1;//1个转换在规则序列中 也就是只转换规则序列1
ADC_Init(ADC1, &ADC_InitStructure);//ADC初始化
ADC_Cmd(ADC1, ENABLE);//开启AD转换器
ADC_ResetCalibration(ADC1);//重置指定的ADC的校准寄存器
while(ADC_GetResetCalibrationStatus(ADC1));//获取ADC重置校准寄存器的状态
ADC_StartCalibration(ADC1);//开始指定ADC的校准状态
while(ADC_GetCalibrationStatus(ADC1));//获取指定ADC的校准程序
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能或者失能指定的ADC的软件转换启动功能
}
下面是中值滤波函数
//中值滤波函数
//total_number为总的数据数
//cut_number为丢弃的高端和低端总的数据数
//保留的数据数必须为2的N次方
u32 Mid_Value_Filter(u16 * databuffer)
{
u32 temp;
u16 i,j;
for (j=0;j<9;j++)
{
for(i=0;i<10-j;i++)
{
if(databuffer[i]>databuffer[i+1]) //升序排列
{
temp=databuffer[i+1];
databuffer[i+1]=databuffer[i];
databuffer[i]=temp;
}
}
}
temp=0;
for(i=3;i<7;i++) //抽取中间4组进行均值运算
{
temp=temp+databuffer[i];
}
temp=temp>>2;
return temp;
}
中值滤波其实就是冒泡排序,然后取中间值。
接下来是AD读取函数
/*******************************************************************************
* 函 数 名 : Get_ADC_Value
* 函数功能 : 获取通道ch的转换值,取times次,然后平均
* 输 入 : ch:通道编号
times:获取次数
* 输 出 : 通道ch的times次转换结果平均值
*******************************************************************************/
u16 Get_ADC_Value(u8 ch,u8 times)
{
u32 temp_val=0;
u16 temp[10];
u8 t;
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); //ADC1,ADC通道,239.5个周期,提高采样时间可以提高精确度
for(t=0;t<times;t++)
{
ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
temp[t]=ADC_GetConversionValue(ADC1);
delay_ms(5);
}
temp_val = Mid_Value_Filter(temp);
return temp_val;
}
上面的AD采样不是连续的,是单次的,每次采样都要软件使能。
下面是各个角度所对应的AD值

基本上是线性,实际上非常准,比人眼还准,哈哈哈
接下来是PWM的配置了,直接给PWM给风扇就好了,没有什么周期,我这里是500Hz,最大的值到35999。
void TIM3_PWMInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO
|RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; //PC7复用为TIM3_CH2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //复用开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
TIM_TimeBaseStructure.TIM_Prescaler =1 ; //预分频值1+1=2,输入计数器36MHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1; //中央对齐模式
TIM_TimeBaseStructure.TIM_Period =35999; //周期35999+1=36000,PWM频率500Hz
TIM_TimeBaseStructure.TIM_ClockDivision = 0x0; //时钟分频因子
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
GPIO_PinRemapConfig(GPIO_FullRemap_TIM3,ENABLE); ////TIM3完全重映射
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; //确定占空比,这个值决定了有效电平的时间
TIM_OC2Init(TIM3, &TIM_OCInitStructure); //输出通道2配置函数
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能预装载寄存器
TIM_Cmd(TIM3,ENABLE); //启动TIM3
}
然后我们还需要定时器配置,为什么需要定时器,其实有很多人不知道这个,我最开始是没有注意这个的,直接放主函数,后来帮别人看滚球平衡的代码时,他也没放定时器里,加了定时器才好多了。代码那样写,积分微分那样写,是基于我们计算每一个误差的时间都是相等的。

微分的思想就是一个线性近似的观念,利用几何的语言就是在函数曲线的局部,用直线代替曲线
,这样子就反映出局部的趋势了。
积分是累加的一种形式,可以简单看成是无限项无限小的和
,可以把它看做“算面积”。
如果我们放在主函数,那么每次执行时间并不一定一样,这样子我们算出的微分反映趋势可能出现偏差,“面积计算”可能出现偏差,我们要避免掉这些。
下面是定时器配置
/*******************************************************************************
* 函 数 名 : TIM4_Init
* 函数功能 : TIM4初始化函数
* 输 入 : per:重装载值
psc:分频系数
* 输 出 : 无
*******************************************************************************/
void TIM4_Init(u16 per,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//使能TIM4时钟
TIM_TimeBaseInitStructure.TIM_Period=per; //自动装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //设置向上计数模式
TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE); //开启定时器中断
TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;//定时器中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority =3; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM4,ENABLE); //使能定时器
}
那么多长时间的定时器中断才是合适的呢?
舵机的控制周期是20ms,控制周期20ms会比较精确,一般风扇用的是直流无刷电机,直流无刷电机的响应速度是非常快的,具体多快,我不清楚,反正不会成为制约我们的因素,它的响应速度不在我们的考虑范围内,可以根据具体情况,调整定时器中断时间。我们只需要考虑的是,采样时间,这个定时器时间要大于采样时间,要不然就没意义了,计算同样的数值。
ADC的采样时间在STM32参考手册上可以找到

我们配置的周期是12MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC分频因子6 72M/6=12
我们的采样周期为239.5
TCONV = 239.5 + 12.5 = 252周期= 252 *1/12MHz = 21μs
中值滤波需要十个数值,这样的话,需要的时间就是210μs了。
实际上定时10毫秒就已经能取得很好的效果了。
TIM4_Init(100,7200-1); //定时10ms
还有就是微分积分时间改变,相对应的PID的系数要改变,积分微分时间减小,系数应该要相应变大。
接下来是定时器中断里怎么处理了

内容非常简单flag是按键按下的标志,加入PID执行函数,然后限制PWM的值,然后设置PWM。
然后就是PID系数的调节,先是我们的P,然后是I,然后是D,我们PWM的最大值是35999,角度0到90度的AD值相差是1200,所以我们的P的系数应该在30以下,你总不能设置为60吧,这样子与目标值相差45度和相差90度的PWM是一样的,就不能很好的体现PID调节的优势了。
最后为了让调节比较方便,可以用串口修改PID的参数,最好掉电不丢失。

这个给大家提供参考,先发送命令值01、02、03、04(16进制),然后发送PID系数或者角度值(16进制)。
整个程序是先有固定的PID系数,然后修改角度值,得到PWM。如果你是先给一个固定的角度值,然后调节PID系数,你会发现调节P和D的系数没有用,这是因为你没有事先给PID的系数,而导致PID输出一直为零也就是下图的绿色部分。

帆板没有动,导致PID->errNow,PID->errOld1,PID->errOld2相同,所以dErrP和dErrD为零。个人感觉要么先给P系数,要么每次给完系数再给角度。

下面是设定的PID系数都储存在备份数据寄存器BKP,然后每次重新上电都给PID重新赋值


上图主函数非常简单,就是初始化和液晶显示。
演示效果
参考代码
题目
本文介绍了使用STM32进行帆板控制系统的PID调节,通过控制风扇转速调节风力,改变帆板转角。详细讲解了PID的工作原理,提供了PID的公式和代码实现,并分享了角度传感器的AD转换、中值滤波以及PWM配置。此外,还讨论了定时器配置的重要性,确保采样时间和计算精度。
2377

被折叠的 条评论
为什么被折叠?



