PID算法与直流电机的PWM技术控制

   PID控制算法是工业控制领域使用最广泛的算法之一,嵌入式系统是工业领域和生活中最常见的智能控制系统之一。本文以单片机控制两个带编码器的直流电机使二者转速保持一致为例讲PID算法与直流电机的PWM技术控制。本文围绕下面几个问题展开:
什么是PID控制算法?

   PID控制算法即比例、积分、微分控制算法。PID控制分为两种:模拟PID和数字PID,前者处理的是连续信号,后者处理的是离散信号。因为单片机只能处理离散信号,本文主要谈后者。对于后者,又分为两种常用的算法即增量式数字PID和位置式数字PID,其中增量式用的更多,所以本文以增量式数字PID为例。PID控制的数学原理比较复杂,限于作者水平,大家可以点击这里进一步了解其控制原理。另外,根据实际控制需求,PID控制算法不一定三个环节都要用,一般常用的还有PI控制、PD控制。
在这里插入图片描述

典型的闭环控制系统结构图

PID控制结构框图

PID控制算法的典型的结构图
  PID控制算法的设计过程中最重要的一个环节就是确定三个参数值的大小的确定,即PID参数整定。为了方便叙述,我们直接用上面的链接里的思路,即确定三个系数A、B、C的大小。其中 :

A = Kp + Ki + Kd,B = Kp + 2 * Kd,C = Kd。 ------- (1)
Kp,Ki,Kd分别是PID控制器的三个参数。
注:上图直接从链接中截取
注:上图直接从链接中截取
   从本人实际的调试经历中,对于电机控制,我一般设置三个参数的大小关系为:Kp>>Kd,Ki,至于具体的数值以及Kp,Ki的相对大小需要根据控制需求和调试过程中输出的情况做一些调整,必要的话,还可能需要做一些算法上的调整,下面分情况详述:

  1. 先定性分析比例、积分、微分三个环节的控制作用:
    比例环节:即时成比例地反应控制系统的偏差信号e(t),偏差一旦产生,控制器立即产生控制作用以减小误差。当偏差e=0时,控制作用也为0。因此,比例控制是基于偏差进行调节的,即有差调节。
    积分环节:能对误差进行记忆,主要用于消除静差,提高系统的无差度,积分作用的强弱取决于积分时间常数Ti,Ti越大,积分作用越弱,反之则越强。值得注意的是,积分作用不宜设置过强,否则控制器输出振荡,会出现两个电机一个转另一个不转或转的慢并轮流交替循环这种极端现象,如果这两个电机是某个小车的两个转向轮,那么小车将会一直走“S”曲线。
    微分环节:能反映偏差信号的变化趋势(变化速率),并能在偏差信号值变得太大之前,在系统中引入一个有效的早期修正信号,从而加快系统的动作速度,减小调节时间。微分系数同样不能太大,否则会降低系统的稳定性。按照上面举的小车的例子,微分作用过大表现在处于稳定状态(即小车保持直线行驶)的小车受到一点点扰动就需要花很长的时间调整回来甚至再也调整不回来了。
       从时间的角度讲,比例作用是针对系统当前误差进行控制,积分作用则针对系统误差的历史,而微分作用则反映了系统误差的变化趋势,这三者的组合是“过去、现在、未来”的完美结合。
  2. 根据上面的定性分析,可以把两个电机的转速差(有正负之分)以一定的频率记录下来并绘制响应曲线(即输出-时间曲线),这个过程可以在上位机上完成,只需要打开单片机的串口,把输出的数据发给上位机即可。然后根据响应曲线调节,三个环节的调节顺序是:先调Kp是输出曲线表现为等幅振荡或近似等幅振荡,然后加上Ki,使输出曲线收敛,最后加上Kd,使响应曲线完全达到控制要求(一般响应快、稳定时无误差、超调小为宜)。
  3. 讲一些特殊情况下的控制算法调整。
    1)控制器的输出溢出,这种情况可能控制作用过强导致,这时要按Ki,Kd,Kp的顺序一个一个地适当减小三个系数,极端情况下可使Ki=0,Kd=0,Kp很小,然后 再看输出曲线。
    2)输出一直在振荡,怎么调控制参数都没用,这时要考虑硬件的问题,或者其他软件问题。
    3)除了上面两种常见的情况,也可以事先了解一下常用的PID控制器的改进方法,比如:死区控制法、遇限削弱积分法、积分分离法、有效偏差法、微分先行法等。再复杂的情况,比如大滞后,可以用串级PID控制算法,对于某一种强扰动也可加上前馈控制算法等。
直流编码电机的控制与转速的检测

先上图看一下它长什么样子:
带编码器的直流减速电机

带编码器的直流减速电机

   编码器:所谓编码,就是它可以把电机的角位移或者角速度信号转换为电脉冲信号,通过对脉冲信号的检测可以确定电机的转速。在上面的结构图中,编码器就是传感器,它将电机转速经测量变速传给单片机。
编码器的驱动:我们用stm32f10xxx系列的单片机作为主控芯片,stm32单片机的定时器外设资源丰富,并且它的通用定时器本身就带有控制编码电机的工作模式,简直就是“量身定做”!
   直流电机的控制:直流电机的转速控制用PWM技术实现,调节PWM的占空比可以线性地改变电机两端的电压,从而控制电机的转速,控制结构图如下:
在这里插入图片描述

直流电机的控制流程
   上面的电机驱动本质是一个功率放大器,将单片机产生的、一定占空比的PWM矩形波转为一定电压电流的模拟信号,进而驱动电机。
程序实现的问题

程序流程如下:
程序流程
下面挑重要的几步讲一下,然后总的源代码程序工程会以压缩包的形式共享出来。

1. 初始化
   初始化这一步主要完成一些全局变量、外设的赋值和配置工作。具体是:PID控制相关参数的初始化,定时器、串口等其他辅助性的外设的配置:

	delay_init(); //延时函数初始化
	TIM5_PWM_Init(1000,9); //PWM初始化 8KHz	
	TIM3_ENC_Init();  //编码器初始化
	TIM4_ENC_Init();  //编码器初始化
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断优先级配置
	pid_init();
	uart_init(115200); //初始化串口,波特率设置为115200

2. PID参数设置

	do //下面的循环语句是为了调节PID参数,串口接收到'0',跳出循环
	{
			switch(go) //注意大小写
			{
				case 'P': mts.Kp += val; go = 0; break;
				case 'I': mts.Ki += val; go = 0; break;
				case 'D': mts.Kd += val; go = 0; break;
				case 'p': mts.Kp -= val; go = 0; break;
				case 'i': mts.Ki -= val; go = 0; break;
				case 'd': mts.Kd -= val; go = 0; break;
				default: break;
			}
			delay_ms(10);
	}while(go != '0');

   这一步是为了后面程序复位后方便调整PID参数,即便于PID参数整定。
3. 循环体

	while(1)
	{
			detPID_PWM_out();//调用PID函数,计算控制增量
			if(sendFlag) //每刷新一次控制输出,往上位机发一次数据
			{
					send_intData(mts.detPWM_out); 
					sendFlag = 0;
			}
			TIM_SetCompare1(TIM5,BASE_SPEED - mts.detPWM_out);	//注意电机的正反转,这里的4个通道可以控制两个电机正反转
			TIM_SetCompare2(TIM5,BASE_SPEED + mts.detPWM_out);		
			TIM_SetCompare3(TIM5,0); //通道3、4为0,此时假设两个电机刚好往同一个方向转,如果转向不一致,请对调硬件或再修改代码,这里只举例说明
			TIM_SetCompare4(TIM5,0);
	}
}

   循环体里面计算控制输出,并根据控制输出调整PWM矩形波的占空比。
4. 中断

/*下面是中断服务函数,定时器中断的优先级高于串口中断*/

void USART1_IRQHandler(void)                	//串口1中断服务程序
	{
		
		if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d(回车) 0x0a(换行)结尾)
			{
				if(sendFlag%2)
				{
						val = USART_ReceiveData(USART1);	//读取接收到的数据
				}
				else
				{
						go = USART_ReceiveData(USART1);	//读取接收到的数据		
				}
				sendFlag = ++sendFlag >= 255?1:sendFlag;						 
			} 
} 
	
void TIM6_IRQHandler(void)   //TIM6中断
{
	if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源 
		{
			TIM_ClearITPendingBit(TIM6, TIM_IT_Update  );  //清除TIMx的中断待处理位:TIM 中断源 
//			(u_16) = TIM_GetCounter(TIM4);
//			(U_16) = TIM_GetCounter(TIM3);
			mts.Ek_2 = mts.Ek_1;
			mts.Ek_1 = mts.Ek;
			mts.Ek = TIM_GetCounter(TIM3) - TIM_GetCounter(TIM4); //计算偏差
			sendFlag = 1;
		}
			
}

   程序中只用了两个中断,一个是串口接收中断,用于接收上位机的命令和数据,以便调试或者监控程序里面一些变量的变化;另一个是定时器中断,为了以50Hz的频率采集编码器反馈的速度信号,同时计算偏差。

总结

   本文只介绍了最简单的PID控制算法C语言实现,上面提到的那几种常用的PID改进算法每一个都可以单独拿出来讲一下如何实现,但限于篇幅且本文重点不是这个所以大家感兴趣的话可以先查查相关的文章,后面有机会我会再单独讲一下我个人对这些算法的理解和实际应用中的实现。本文的基于uKeil的C源码工程共享在公众号“24K纯学渣”上面,回复“PID电机控制”即可获取。另外,限于作者水平,如有描述错误或不妥之处,欢迎大家在下面评论指出。如果希望进一步的交流学习,可以在公众号上联系作者,任何学习上的交流本人都非常欢迎!

  • 28
    点赞
  • 408
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
您好!下面是一个基于PID算法控制STM32直流减速电机的PWM驱动的代码示例: ```C #include "stm32f4xx.h" // PID参数 float Kp = 1.0; float Ki = 0.5; float Kd = 0.2; // 电机速度设定值 int targetSpeed = 1000; // PID变量 float error = 0; float integral = 0; float derivative = 0; float lastError = 0; // PWM输出频率和分辨率 int pwmFrequency = 20000; // 20kHz int pwmResolution = 1000; // 10-bit分辨率 // 初始化PWM void PWM_init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_TIM3); GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_TIM3); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period = pwmResolution - 1; TIM_TimeBaseStructure.TIM_Prescaler = (SystemCoreClock / pwmFrequency) - 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); } // 初始化编码器 void Encoder_init(void) { RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource0, GPIO_AF_TIM5); GPIO_PinAFConfig(GPIOA, GPIO_PinSource1, GPIO_AF_TIM5); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); TIM_EncoderInterfaceConfig(TIM5, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM5, ENABLE); } // 获取编码器计数器值 int Encoder_getCount(void) { return (int)TIM_GetCounter(TIM5); } // PID控制 void PID_control(void) { int currentSpeed = Encoder_getCount(); // 计算误差 error = targetSpeed - currentSpeed; // 计算积分项 integral += error; // 计算微分项 derivative = error - lastError; lastError = error; // 计算PID输出 float output = Kp * error + Ki * integral + Kd * derivative; // 限制输出范围 if (output > pwmResolution) { output = pwmResolution; } else if (output < -pwmResolution) { output = -pwmResolution; } // 设置PWM占空比 TIM_SetCompare1(TIM3, (uint16_t)(pwmResolution - fabs(output))); } int main(void) { PWM_init(); Encoder_init(); while (1) { PID_control(); } } ``` 这段代码使用了STM32的TIM3模块作为PWM输出,使用TIM5模块作为编码器输入。其中,`PWM_init()`函数用于初始化PWM输出,`Encoder_init()`函数用于初始化编码器。`PID_control()`函数用于进行PID控制,计算PID输出,并设置PWM占空比。 请根据您的具体硬件和需求进行适当的修改和调整。希望对您有所帮助!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值