文章目录
-
- 一、项目概述与学习目标
-
- [1.1 项目背景](#1.1 项目背景)
- [1.2 学习目标](#1.2 学习目标)
- [1.3 系统整体架构](#1.3 系统整体架构)
- 二、硬件系统设计
-
- [2.1 硬件选型说明](#2.1 硬件选型说明)
- [2.2 硬件连接原理图](#2.2 硬件连接原理图)
- [2.3 硬件检查与测试](#2.3 硬件检查与测试)
- 三、PID控制算法原理
-
- [3.1 _pid算法数学模型](#3.1 _pid算法数学模型)
- 3.2_pid参数调节方法
- 3.3_pid算法流程
- 四、开发环境搭建
-
- [4.1 开发工具介绍](#4.1 开发工具介绍)
- [4.2 项目工程创建](#4.2 项目工程创建)
- 五、程序代码详解
-
- [5.1 头文件与宏定义](#5.1 头文件与宏定义)
- [5.2 PID算法实现](#5.2 PID算法实现)
- [5.3 电机驱动实现](#5.3 电机驱动实现)
- [5.4 主程序与中断处理](#5.4 主程序与中断处理)
- 六、系统调试与优化
-
- [6.1 调试方法与步骤](#6.1 调试方法与步骤)
- [6.2 常见问题与解决方法](#6.2 常见问题与解决方法)
- [6.3 性能优化技巧](#6.3 性能优化技巧)
- 七、扩展应用与总结
-
- [7.1 实际应用场景](#7.1 实际应用场景)
- [7.2 系统升级建议](#7.2 系统升级建议)
- [7.3 项目总结](#7.3 项目总结)
一、项目概述与学习目标
1.1 项目背景
在现代工业控制和智能硬件领域,直流电机的精确速度控制是一个非常重要的技术课题。传统的开环控制方式虽然简单,但无法应对负载变化带来的速度波动,也无法保证系统的稳态精度。为了实现高精度的速度控制,我们需要引入闭环控制系统,而PID控制器正是工业界最经典、最广泛使用的闭环控制算法。
本项目将带领大家从零开始,使用STM32F103C8T6单片机实现一个完整的直流电机闭环调速系统。我们将深入学习PID算法的原理,掌握STM32的定时器捕获、PWM输出、GPIO配置等外设的使用方法,最终实现一个可以实际运行的电机调速系统。
1.2 学习目标
通过本项目的学习,您将掌握以下技能:
第一,深入理解PID控制算法的数学原理和工作机制。PID代表比例(Proportional)、积分(Integral)和微分(Differential)三个控制环节,每个环节都有其独特的控制作用。比例环节根据当前误差进行调节,积分环节消除稳态误差,微分环节预测未来误差趋势并提前进行补偿。
第二,掌握STM32单片机的基本编程方法。包括系统时钟配置、GPIO端口配置、定时器中断配置、PWM输出配置以及输入捕获配置等。这些是嵌入式开发的基础技能。
第三,学会直流电机驱动电路的设计和搭建。我们将使用L298N电机驱动模块,它是目前最常用的直流电机驱动方案之一,可以安全地控制电机的转速和方向。
第四,具备完整的嵌入式项目开发能力。从需求分析、硬件设计、代码编写到调试优化,完整体验一个工程项目的开发流程。
1.3 系统整体架构
本系统采用典型的闭环控制架构,主要包含以下几个部分:
控制器部分使用STM32F103C8T6作为主控芯片,它负责运行PID控制算法、生成PWM信号、捕获编码器脉冲等核心功能。STM32F103系列是ST公司推出的一款经典型32位ARM Cortex-M3内核单片机,具有丰富的外设资源和强大的处理能力,非常适合作为中小型嵌入式项目的核心控制器。
执行器部分采用L298N双H桥电机驱动芯片,它可以同时驱动两台直流电机或者一台两相步进电机。L298N支持PWM调速和方向控制,最大驱动电流可达2A,足以满足大多数小型直流电机的驱动需求。
被控对象是一台带有光电编码器的直流减速电机。光电编码器可以输出与电机转速成正比的脉冲信号,我们通过捕获这些脉冲来实时获取电机的实际转速,从而实现闭环控制。
反馈环节由STM32的定时器捕获功能实现。我们将编码器的A相和B相输出分别连接到STM32的捕获引脚,通过测量脉冲周期或计数脉冲数量来计算电机的当前转速。
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 直流电机闭环调速系统架构图 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌───────┐ │
│ │ 设定速度 │────────▶│ PID │────────▶│ PWM │────────▶│ L298N │ │
│ │ (目标值) │ │ 控制器 │ │ 生成 │ │ 驱动 │ │
│ └──────────┘ └──────────────┘ └────────────┘ └───┬───┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌────────┐ │
│ │ 速度反馈 │◀─────────────────────────────────│ 直流 │ │
│ │ (实际值) │ 编码器脉冲 │ 电机 │ │
│ └──────────────┘ └────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ 闭环控制流程 │ │
│ │ 设定速度 ──▶ 误差计算 ──▶ PID运算 ──▶ PWM输出 ──▶ 电机调速 ──▶ 速度反馈 │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
二、硬件系统设计
2.1 硬件选型说明
在开始动手之前,我们先来详细了解一下整个系统所需的硬件组件及其作用。
主控芯片:STM32F103C8T6
这款芯片是STM32家族中最经典的型号之一,采用ARM Cortex-M3内核,最高工作频率72MHz。它内置了丰富的外设资源,包括多个定时器、多个USART通信接口、SPI接口、I2C接口、ADC模数转换器等。对于本项目来说,我们需要使用定时器来生成PWM信号和捕获编码器脉冲,这些功能在STM32F103C8T6上都可以轻松实现。
从引脚数量来看,STM32F103C8T6采用LQFP-48封装,共有48个引脚,其中大部分可以作为GPIO使用,足够满足我们的项目需求。从价格来看,这款芯片的价格非常亲民,单片价格在10元左右,是性价比极高的选择。
电机驱动模块:L298N
L298N是一款经典的双H桥电机驱动芯片,它可以驱动两台直流电机或者一台两相步进电机。模块版本集成了电源电路和逻辑电平转换电路,可以直接与STM32单片机连接。
L298N模块的主要特性包括:支持双路电机驱动,每路最大输出电流2A;支持PWM调速,可以通过改变PWM占空比来调节电机转速;支持正反转控制,通过控制IN1和IN2引脚的电平可以改变电机转向;内置5V稳压电路,可以为STM32开发板供电。
使用L298N时需要注意,由于其内部采用逻辑门电路,存在约1.5V-2V的压降,在低电压供电时可能影响电机性能。另外,当驱动电流较大时需要加装散热片,以防止芯片过热损坏。
直流减速电机带编码器
我们需要选择一款带有光电编码器的直流减速电机。光电编码器是一种将机械角度转换为电信号的传感器,它通常包含一个带有狭缝的码盘和一个光电开关。当电机转动时,码盘随之旋转,光电开关检测到狭缝的变化并输出脉冲信号。
本项目推荐使用额定电压12V、减速比1:30的直流减速电机,编码器采用AB相双通道输出,分辨率为单圈11个脉冲(经过30:1减速后,实际电机轴每转一圈输出330个脉冲)。这种配置可以获得较好的速度控制精度,同时电机扭矩也足够驱动小型车辆或机械装置。
在购买电机时,需要注意编码器的输出信号类型。大多数编码器输出的是两相正交脉冲(A相和B相),它们之间有90度的相位差,可以用于判断电机转向。如果只是单相输出,则只能测量速度,无法判断转向。
其他辅助材料
除了上述核心部件外,我们还需要准备以下材料和工具:
电源方面,需要一个12V直流电源适配器,电流至少2A。可以使用实验室电源或者成品电源模块。也可以使用3S锂电池(11.1V)供电。
连接线材方面,需要准备一些杜邦线(公对公、公对母、母对母)、面包板或洞洞板、电工胶带、热缩管等。建议使用不同颜色的杜邦线来区分电源正负、信号线等功能,这样可以大大提高接线效率和调试便利性。
调试工具方面,需要准备一个ST-Link下载器用于程序烧录,一个数字万用表用于检测电压和通断,一个示波器(可选)用于观察PWM波形和编码器信号。
2.2 硬件连接原理图
正确的硬件连接是项目成功的前提。下面我们详细说明各个模块之间的连接关系。
STM32与L298N的连接
STM32需要向L298N输出两类控制信号:方向控制信号和速度控制信号。
方向控制使用两个GPIO引脚。以左电机为例,IN1连接PA4,IN2连接PA5。当IN1=1且IN2=0时,电机正转;当IN1=0且IN2=1时,电机反转;同时为0或同时为1时,电机停止。右电机使用PA6和PA7两个引脚。
速度控制使用PWM信号。PWM(脉冲宽度调制)是一种通过改变脉冲占空比来调节输出功率的技术。我们将STM32定时器产生的PWM信号连接到L298N的ENA(使能)引脚。ENA引脚接收PWM信号后,会根据占空比的大小来调节输出到电机的平均电压,从而实现调速。
具体连接如下:PA0输出左电机PWM(连接到L298N的ENA),PA1输出右电机PWM(连接到L298N的ENB)。
STM32与编码器的连接
编码器通常有四根输出线:A相、B相、VCC(5V)和GND。A相和B相输出方波信号,VCC和GND用于供电。
我们将编码器的A相连接到STM32的PC6引脚,B相连接到PC7引脚。STM32的定时器具有编码器模式,可以自动对A相和B相的脉冲进行计数,我们只需要读取计数器的值就可以获得电机的位置信息,进一步计算就可以得到速度信息。
需要特别注意的是,编码器必须使用5V供电,而STM32的GPIO引脚输出的是3.3V电平。虽然大多数编码器可以接受3.3V信号,但为了保证信号的稳定性,建议使用5V供电。如果编码器输出的是5V电平信号,需要使用电平转换电路或者选择支持3.3V的编码器。
电源系统连接
整个系统的电源分配如下:12V电源正极连接到L298N的+12V输入,负极接地。L298N的+5V输出(板上稳压芯片提供)可以为编码器供电,同时也可以为STM32开发板供电。注意,如果使用L298N为STM32供电,需要确保跳线帽连接在ENA和ENB引脚上。
在连接电源时,一定要注意极性,避免反接造成器件损坏。建议在电源输入端串联一个保险丝或自恢复保险丝,以提供过流保护。
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 硬件系统接线图 │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 12V电源适配器 │
│ │ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ L298N │ │
│ ┌─────┤ 电机驱动 ├─────┐ │
│ │ │ │ │ │
│ │ │ IN1 IN2 │ │ │
│ │ │ IN3 IN4 │ │ │
│ │ │ ENA ENB │ │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ┌──────┴──────┐ │ │
│ │ │ +5V GND │ │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ┌───────┴───────┐ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ PA4 │ │ PA5 │ │ PA6 │ │ PA7 │ 方向控制 │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │IN1 │ │IN2 │ │IN3 │ │IN4 │ │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │ │
│ │ ┌────┴────┐ │ ┌────┴────┐ │
│ │ │ STM32 │ │ │ │ │
│ │ │F103C8T6 │ │ │ │ │
│ │ └────┬────┘ │ └────┬────┘ │
│ │ │ │ │ │
│ │ ┌────┴────┐ │ ┌────┴────┐ │
│ │ │ PA0 PA1 │ │ │ │ │
│ │ │ PWM1 PWM2│ │ │ │ │
│ │ │ ENA ENB│ │ │ │ │
│ └──┴─────────┘ │ │ │ │
│ │ │ │
│ ┌─────────────────────┴─────────┐ │
│ │ │ │
│ │ 编码器(左轮) 编码器(右轮) │
│ │ A相:PC6 A相:PC8 │
│ │ B相:PC7 B相:PC9 │
│ │ VCC:5V VCC:5V │
│ │ GND:GND GND:GND │
│ └──────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────┘
2.3 硬件检查与测试
在通电测试之前,我们需要仔细检查所有连接,确保没有错误和短路。以下是检查要点:
首先检查电源连接。测量12V电源适配器的输出电压是否稳定在11V-12.5V范围内。检查L298N模块的电源输入端是否有滤波电容,电压是否正常。
其次检查信号连接。使用万表蜂鸣档检查相邻的GPIO引脚之间是否有短路,特别是在排针密集的区域。确认每一根信号线都正确连接到对应的引脚,没有错位。
然后检查编码器连接。编码器的A相和B相输出不能接反,虽然不影响速度测量,但会导致转向判断错误。编码器的VCC和GND不能接反,否则会损坏编码器。
最后进行通电测试。先不要连接电机,只给系统上电。测量STM32开发板的3.3V和5V电源是否正常。使用示波器观察PWM输出引脚,确认PWM信号正常生成。检查编码器输出,在手动转动电机轴时,应该能看到A相和B相都有脉冲输出。
三、PID控制算法原理
3.1 _pid算法数学模型
PID控制算法是自动控制原理中最基础也是最重要的算法之一。它的核心思想是根据系统的当前状态(被控量)与期望状态(设定值)之间的偏差,来决定控制器的输出。PID三个字母分别代表三个控制环节:比例(Proportional)、积分(Integral)和微分(Differential)。
让我们先定义几个基本变量。设定值(Setpoint)是我们期望达到的目标值,在本项目中就是目标转速。实际值(Process Value)是系统当前的实际状态,通过传感器测量获得。误差(Error)是设定值与实际值之间的差值,即Error = Setpoint - Process Value。控制量(Control Output)是PID控制器计算出的输出值,用于驱动执行器。
比例环节(P)
比例环节是最简单的控制方式,它的输出与误差成正比。比例控制的公式为:
Output_Kp = Kp × Error
其中Kp是比例增益系数。比例控制的作用是:當误差为正(实际值小于设定值)时,输出正向控制量,使系统向设定值方向移动;當误差为负(实际值大于设定值)时,输出负向控制量,抑制系统继续向设定值的反方向移动。
比例控制的特点是响应速度快,但存在稳态误差。设想一个场景:假设电机在空载时需要30%的PWM占空比才能达到目标转速,当加上负载后,需要50%的占空比才能维持同样的转速。如果只使用比例控制,系统会稳定在某个占空比,此时误差为零,但稳态误差仍然存在。
积分环节(I)
积分环节的引入就是为了消除稳态误差。积分控制的输出与误差的积分成正比,公式为:
Output_Ki = Ki × ∫Error(t)dt
积分控制的特点是:只要存在误差,积分项就会不断累积,控制输出也会不断增加,直到误差被消除为止。这完美解决了比例控制无法消除稳态误差的问题。
积分控制也有其副作用。由于积分项会累积,如果误差长期存在,积分项可能变得非常大,导致系统超调甚至振荡。因此,积分系数通常需要设置得比较小。
在实际编程中,我们通常使用累加求和的方式来近似积分:
integral = integral + Error × dt
Output_Ki = Ki × integral
其中dt是控制周期。积分项需要设置上限(积分限幅),防止积分饱和。
微分环节(D)
微分环节根据误差的变化趋势来进行控制,可以预测未来的误差并提前采取行动。微分控制的公式为:
Output_Kd = Kd × d(Error)/dt
微分控制的作用是:当误差正在减小时,输出一个反向的控制量来抑制系统的惯性,使系统尽快稳定;当误差正在增大时,输出一个同向的控制量来加速系统的响应。
微分控制可以有效抑制系统的超调和振荡,提高系统的稳定性。但微分项对噪声非常敏感,因为噪声会导致误差的快速变化,从而产生很大的微分输出。因此,在实际应用中通常需要对微分项进行滤波处理。
完整的PID公式
将三个环节结合起来,就得到了完整的PID控制公式:
Output = Kp × Error + Ki × ∫Error(t)dt + Kd × d(Error)/dt
在实际控制系统中,我们通常使用位置式PID或增量式PID两种实现方式。
位置式PID直接计算控制器的输出值,公式为:
Output = Kp × Error + Ki × integral + Kd × (Error - Error_prev) / dt
其中Error是当前误差,Error_prev是上一次误差。这种方式计算简单,但每次输出都与历史状态有关。
增量式PID计算的是控制量的增量,公式为:
ΔOutput = Kp × (Error - Error_prev) + Ki × Error + Kd × (Error - 2×Error_prev + Error_prev2) / dt
增量式PID的优势在于:只与最近三次误差值有关,计算量小;输出是增量值,可以用于需要增量控制的场合;更容易实现无扰切换。在电机控制中,我们通常使用增量式PID。
3.2_pid参数调节方法
PID参数的调节(也称为整定)是PID控制应用中最重要的环节。参数设置不合理会导致系统响应慢、超调大甚至振荡不稳定。下面介绍几种常用的PID参数调节方法。
试凑法
试凑法是最常用的方法,通过反复试验来调整参数。具体步骤如下:
首先调节比例环节。将Ki和Kd设为0,逐步增大Kp,观察系统的响应。理想的响应是:系统能够快速响应误差,且没有持续的振荡。当增大Kp到系统开始出现振荡时,稍微减小一点,这个值就是合适的Kp。
然后调节积分环节。在确定Kp后,逐步增大Ki,消除稳态误差。观察系统是否出现振荡,如果有则减小Ki。积分环节的调节目标是:既能够消除稳态误差,又不会引起明显的振荡。
最后调节微分环节。微分环节主要用于抑制超调和提高响应速度。在调节时,应该从较小的值开始,逐步增加,直到系统响应既快速又平稳。
临界比例法
临界比例法是一种更加系统的调节方法。具体步骤如下:
第一步,将积分时间和微分时间都设为0,系统变为纯比例控制。
第二步,逐渐增大比例增益,直到系统出现等幅振荡。记录此时的比例增益Ku和振荡周期Tu。
第三步,根据经验公式计算PID参数。对于PID控制器:Kp = 0.6×Ku,Ki = 2×Ku/Tu,Kd = Ku×Tu/8。
第四步,将计算出的参数代入系统,观察效果并进行微调。
本项目的参数整定建议
对于直流电机调速系统,由于电机本身的惯性较大,建议采用以下参数范围作为起点:Kp取0.1-0.5,Ki取0.01-0.05,Kd取0.01-0.1。
在具体调节时,可以参考以下规律:如果系统响应太慢,可以增大Kp;如果系统超调太大,可以减小Kp或增大Kd;如果稳态误差太大,可以增大Ki;如果系统振荡频繁,可以减小Ki和Kd。
PID参数调节是一个需要耐心和经验的过程。建议先在仿真环境中测试,确认算法正确后再到实际硬件上调试。实际调试时也要循序渐进,先小后大,防止损坏硬件。
3.3_pid算法流程
下面我们用流程图来展示PID控制算法的工作流程。这个流程图展示了从误差计算到控制输出,再到电机响应的完整闭环过程。
输出处理
PID计算
主控制循环
初始化阶段
系统上电
配置GPIO
配置定时器PWM输出
配置编码器捕获
初始化PID参数
开启中断和PWM输出
获取目标设定速度
读取编码器计数值
计算电机实际速度
计算速度误差
Error = 设定速度 - 实际速度
积分累积
计算微分项
计算比例项 Kp × Error
计算积分项 Ki × integral
计算微分项 Kd × diff
三项求和得到控制量
限幅处理
检查是否反转
更新PWM占空比
更新电机方向控制
延时等待下一个控制周期
四、开发环境搭建
4.1 开发工具介绍
要进行STM32开发,我们需要准备以下软件工具:
Keil MDK
Keil MDK(Microcontroller Development Kit)是ARM公司官方推荐的嵌入式开发环境,是目前最流行的STM32开发工具之一。Keil MDK集成了代码编辑器、编译器、调试器等功能,支持STM32全系列芯片。
Keil MDK的安装包可以从ARM官网或国内镜像站点下载。安装过程中需要选择对应的芯片支持包。安装完成后,需要进行注册,可以选择使用免费评估版(代码大小限制32KB)或购买正式版。
STM32CubeMX
STM32CubeMX是ST公司提供的图形化配置工具,可以通过图形界面生成STM32外设初始化代码。使用STM32CubeMX可以大大减少底层配置的工作量,让开发者专注于应用层代码的编写。
STM32CubeMX支持所有STM32系列芯片,可以自动生成Keil、IAR、GCC等主流编译器的项目文件。它还集成了HAL库和LL库,HAL库抽象层次较高,使用方便,LL库更接近底层,性能更好。
ST-Link驱动
ST-Link是ST公司推出的调试下载器,用于将程序烧录到STM32芯片中。我们需要在电脑上安装ST-Link驱动程序才能正常使用调试功能。
驱动程序安装很简单:将ST-Link通过USB线连接到电脑,Windows系统会自动识别新硬件并尝试安装驱动。如果自动安装失败,可以从ST官网下载对应的驱动手动安装。
4.2 项目工程创建
下面我们详细讲解如何使用Keil MDK和STM32CubeMX创建一个完整的STM32项目。
首先打开STM32CubeMX,点击"New Project"创建新项目。在芯片选择界面,搜索"STM32F103C8",找到"STM32F103C8Tx"芯片,点击选中。然后点击"Start Project"开始配置。
在Pinout & Configuration界面,我们需要在左侧Categories列表中依次配置各个外设。
展开"System Core",点击"GPIO",配置GPIO引脚。根据硬件连接图,我们需要设置以下引脚:
PA4设置为"GPIO_Output"模式,作为左电机方向控制IN1;PA5设置为"GPIO_Output"模式,作为左电机方向控制IN2;PA6设置为"GPIO_Output"模式,作为右电机方向控制IN1;PA7设置为"GPIO_Output"模式,作为右电机方向控制IN2。
展开"Timers",点击"TIM1",配置PWM输出功能。将Channel1设置为"PWM Generation CH1",Channel2设置为"PWM Generation CH2"。点击"Parameter Settings",将Counter Period设置为1999(即PWM频率为72000/(1999+1)=36KHz),这样PWM的占空比分辨率可以达到2000级。
点击"TIM3",配置编码器捕获功能。将Combined Channels设置为"Encoder Mode",在Pin Selection中配置Channel1和Channel2分别对应PC6和PC7。点击"Parameter Settings",将Encoder Mode设置为"Encoder Mode TI1 and TI2",Counter Period设置为65535。
展开"NVIC Settings",勾选TIM3的中断使能,这样可以在定时器中断中读取编码器计数值。
最后点击"Project"菜单,设置项目名称为"MotorPID",选择项目保存路径,Toolchain选择"MDK-ARM"。点击"Code Generator",勾选"Generate peripheral initialization as a pair of '.c/.h' files per peripheral",这样会将每个外设的初始化代码分别生成到独立的文件中。点击"GENERATE CODE"生成项目代码。
打开生成的Keil项目,点击Options for Target按钮(看起来像魔术棒的图标),在Target标签页中,将Xtal(MHz)设置为8.0(外部晶振频率)。在Debug标签页中,选择ST-Link Debugger,点击Settings按钮,确认SW Device能够识别到芯片。
点击Project菜单下的Rebuild all target files编译项目,确认没有错误后就可以开始编写应用层代码了。
五、程序代码详解
5.1 头文件与宏定义
首先,我们需要创建项目的头文件,定义各种常量、结构和函数声明。
文件名:MotorPID.h
c
#ifndef __MOTOR_PID_H
#define __MOTOR_PID_H
#include "main.h"
#include "tim.h"
#include "gpio.h"
// PID参数结构体
typedef struct {
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float target; // 目标值
float current; // 当前值(实际值)
float error; // 当前误差
float last_error; // 上一次误差
float prev_error; // 上上次误差
float integral; // 积分累积
float output; // PID输出
float max_output; // 输出上限
float min_output; // 输出下限
float max_integral; // 积分上限
} PID_Controller;
// 电机状态结构体
typedef struct {
int16_t encoder_count; // 编码器计数值
float speed; // 当前速度(rpm)
float target_speed; // 目标速度(rpm)
uint8_t direction; // 旋转方向:0-停止,1-正转,2-反转
PID_Controller pid; // PID控制器
GPIO_TypeDef* dir1_port; // 方向控制引脚1端口
uint16_t dir1_pin; // 方向控制引脚1编号
GPIO_TypeDef* dir2_port; // 方向控制引脚2端口
uint16_t dir2_pin; // 方向控制引脚2编号
TIM_HandleTypeDef* pwm_timer; // PWM定时器句柄
uint32_t pwm_channel; // PWM通道
TIM_HandleTypeDef* encoder_timer; // 编码器定时器句柄
} Motor_TypeDef;
// 函数声明
void Motor_Init(Motor_TypeDef* motor);
void Motor_Set_Speed(Motor_TypeDef* motor, float speed_rpm);
void Motor_Stop(Motor_TypeDef* motor);
void Motor_Set_Direction(Motor_TypeDef* motor, uint8_t direction);
void Motor_Update(Motor_TypeDef* motor);
void Motor_Calculate_Speed(Motor_TypeDef* motor);
void PID_Init(PID_Controller* pid, float kp, float ki, float kd);
void PID_Set_Target(PID_Controller* pid, float target);
float PID_Calculate(PID_Controller* pid, float current);
#endif
这个头文件定义了PID控制器和电机控制所需的数据结构。PID_Controller结构体包含了PID算法的所有参数,包括三个系数、误差值、积分项和输出值。Motor_TypeDef结构体封装了单个电机的所有信息,包括编码器数据、PID控制器和硬件配置。
5.2 PID算法实现
接下来是PID算法的具体实现代码。
文件名:pid.c
c
#include "MotorPID.h"
/**
* PID控制器初始化
* @param pid: PID控制器结构体指针
* @param kp: 比例系数
* @param ki: 积分系数
* @param kd: 微分系数
*/
void PID_Init(PID_Controller* pid, float kp, float ki, float kd) {
pid->Kp = kp;
pid->Ki = ki;
pid->Kd = kd;
pid->target = 0.0f;
pid->current = 0.0f;
pid->error = 0.0f;
pid->last_error = 0.0f;
pid->prev_error = 0.0f;
pid->integral = 0.0f;
pid->output = 0.0f;
pid->max_output = 1000.0f; // PWM最大值
pid->min_output = -1000.0f; // PWM最小值
pid->max_integral = 500.0f; // 积分饱和值
}
/**
* 设置PID控制器的目标值
* @param pid: PID控制器结构体指针
* @param target: 目标值
*/
void PID_Set_Target(PID_Controller* pid, float target) {
pid->target = target;
}
/**
* PID计算函数(增量式PID)
* @param pid: PID控制器结构体指针
* @param current: 当前实际值
* @return: PID控制器输出
*/
float PID_Calculate(PID_Controller* pid, float current) {
float p_term, i_term, d_term;
float delta_output;
// 更新当前值和历史误差
pid->current = current;
pid->prev_error = pid->last_error;
pid->last_error = pid->error;
pid->error = pid->target - pid->current;
// 比例环节:P = Kp * (error - last_error)
// 使用误差变化量而非绝对误差,这是增量式PID的特点
p_term = pid->Kp * (pid->error - pid->last_error);
// 积分环节:I = Ki * error
i_term = pid->Ki * pid->error;
// 积分累积(带饱和限制)
pid->integral += i_term;
// 积分限幅,防止积分饱和
if (pid->integral > pid->max_integral) {
pid->integral = pid->max_integral;
} else if (pid->integral < -pid->max_integral) {
pid->integral = -pid->max_integral;
}
// 微分环节:D = Kd * (error - 2*last_error + prev_error)
// 二阶微分可以更好地预测系统趋势
d_term = pid->Kd * (pid->error - 2.0f * pid->last_error + pid->prev_error);
// 计算控制增量
delta_output = p_term + i_term + d_term;
// 更新输出(增量式PID是累加的关系)
pid->output += delta_output;
// 输出限幅
if (pid->output > pid->max_output) {
pid->output = pid->max_output;
} else if (pid->output < pid->min_output) {
pid->output = pid->min_output;
}
return pid->output;
}
/**
* 重置PID控制器
* @param pid: PID控制器结构体指针
*/
void PID_Reset(PID_Controller* pid) {
pid->error = 0.0f;
pid->last_error = 0.0f;
pid->prev_error = 0.0f;
pid->integral = 0.0f;
pid->output = 0.0f;
}
这段代码实现了增量式PID算法。增量式PID与位置式PID的主要区别在于:位置式直接计算控制器的绝对输出值,而增量式计算的是控制量的变化量。增量式PID更适合用于电机控制,因为它更容易实现无扰切换(即在控制器启动或切换时不会产生突变)。
在实现中,我们使用了误差变化量(error - last_error)作为比例项的输入,这比使用绝对误差更符合增量式的思想。积分项采用了抗积分饱和措施,当积分累积超过设定上限时会进行限幅,防止积分项过大导致系统超调。微分项使用了二阶差分形式,可以更好地预测误差的变化趋势。
5.3 电机驱动实现
接下来是电机控制的实现代码。
文件名:motor.c
c
#include "MotorPID.h"
// 编码器相关常量
#define ENCODER_PERIOD 65535 // 编码器计数器最大值
#define ENCODER_PPR 330 // 编码器每圈脉冲数(11 PPR * 30 减速比)
#define COUNT_PERIOD_MS 10 // 速度计算周期(毫秒)
#define SECONDS_PER_MINUTE 60.0f
/**
* 电机初始化
* @param motor: 电机结构体指针
*/
void Motor_Init(Motor_TypeDef* motor) {
// 初始化方向控制引脚为低电平(停止状态)
HAL_GPIO_WritePin(motor->dir1_port, motor->dir1_pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(motor->dir2_port, motor->dir2_pin, GPIO_PIN_RESET);
// 初始化PWM占空比为0
HAL_TIM_PWM_Stop(motor->pwm_timer, motor->pwm_channel);
// 启动编码器定时器
HAL_TIM_Encoder_Start(motor->encoder_timer, TIM_CHANNEL_ALL);
// 初始化编码器计数值
motor->encoder_count = 0;
motor->speed = 0.0f;
motor->direction = 0;
// 初始化PID控制器(使用默认参数,后续可以通过参数整定优化)
PID_Init(&motor->pid, 0.3f, 0.05f, 0.01f);
PID_Set_Target(&motor->pid, 0.0f);
// 设置PID输出范围
motor->pid.max_output = 1000.0f;
motor->pid.min_output = -1000.0f;
}
/**
* 设置电机目标速度
* @param motor: 电机结构体指针
* @param speed_rpm: 目标速度(rpm)
*/
void Motor_Set_Speed(Motor_TypeDef* motor, float speed_rpm) {
motor->target_speed = speed_rpm;
PID_Set_Target(&motor->pid, speed_rpm);
}
/**
* 停止电机
* @param motor: 电机结构体指针
*/
void Motor_Stop(Motor_TypeDef* motor) {
// 设置PWM占空比为0
__HAL_TIM_SET_COMPARE(motor->pwm_timer, motor->pwm_channel, 0);
// 设置方向控制引脚为停止状态
HAL_GPIO_WritePin(motor->dir1_port, motor->dir1_pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(motor->dir2_port, motor->dir2_pin, GPIO_PIN_RESET);
// 停止PID计算
motor->target_speed = 0.0f;
PID_Set_Target(&motor->pid, 0.0f);
PID_Reset(&motor->pid);
motor->direction = 0;
}
/**
* 设置电机方向
* @param motor: 电机结构体指针
* @param direction: 方向(0-停止,1-正转,2-反转)
*/
void Motor_Set_Direction(Motor_TypeDef* motor, uint8_t direction) {
motor->direction = direction;
switch (direction) {
case 1: // 正转
HAL_GPIO_WritePin(motor->dir1_port, motor->dir1_pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(motor->dir2_port, motor->dir2_pin, GPIO_PIN_RESET);
break;
case 2: // 反转
HAL_GPIO_WritePin(motor->dir1_port, motor->dir1_pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(motor->dir2_port, motor->dir2_pin, GPIO_PIN_SET);
break;
default: // 停止
HAL_GPIO_WritePin(motor->dir1_port, motor->dir1_pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(motor->dir2_port, motor->dir2_pin, GPIO_PIN_RESET);
break;
}
}
/**
* 计算电机速度(从编码器计数值)
* @param motor: 电机结构体指针
*/
void Motor_Calculate_Speed(Motor_TypeDef* motor) {
static int16_t last_count = 0;
int16_t current_count;
int16_t count_diff;
// 读取编码器当前计数值
current_count = (int16_t)__HAL_TIM_GET_COUNTER(motor->encoder_timer);
// 计算计数差值(处理计数器溢出情况)
count_diff = current_count - last_count;
// 处理计数器溢出/下溢
if (count_diff > ENCODER_PERIOD / 2) {
count_diff -= ENCODER_PERIOD;
} else if (count_diff < -ENCODER_PERIOD / 2) {
count_diff += ENCODER_PERIOD;
}
// 更新上次计数值
last_count = current_count;
// 将计数差值转换为速度(rpm)
// 速度 = (计数差值 / 每圈脉冲数) * (60000ms / 采样周期ms) / 减速比
motor->speed = (float)count_diff * SECONDS_PER_MINUTE * 1000.0f /
(ENCODER_PPR * COUNT_PERIOD_MS);
// 过滤异常速度值(去除突变)
if (motor->speed > 10000.0f) motor->speed = 0.0f;
if (motor->speed < -10000.0f) motor->speed = 0.0f;
}
/**
* 更新电机控制(PID控制主函数)
* @param motor: 电机结构体指针
*/
void Motor_Update(Motor_TypeDef* motor) {
float pid_output;
uint16_t pwm_duty;
// 计算当前电机速度
Motor_Calculate_Speed(motor);
// 使用PID算法计算控制输出
pid_output = PID_Calculate(&motor->pid, motor->speed);
// 根据PID输出判断方向并设置方向控制
if (pid_output > 10.0f) {
// 正转
Motor_Set_Direction(motor, 1);
pwm_duty = (uint16_t)pid_output;
} else if (pid_output < -10.0f) {
// 反转
Motor_Set_Direction(motor, 2);
pwm_duty = (uint16_t)(-pid_output);
} else {
// 速度很小,直接停止
Motor_Set_Direction(motor, 0);
pwm_duty = 0;
}
// 更新PWM占空比
// PWM计数器周期为2000,所以占空比直接使用PID输出值
__HAL_TIM_SET_COMPARE(motor->pwm_timer, motor->pwm_channel, pwm_duty);
}
电机驱动代码是整个系统的核心。Motor_Init函数负责初始化电机的各个部分,包括设置GPIO引脚状态、配置PWM输出参数和启动编码器计数器。
Motor_Calculate_Speed函数实现了从编码器计数值到速度值的转换。这里需要特别注意处理编码器计数器的溢出情况。当计数器从65535增加到0时,会产生一个负的跳变;反之从0到65535会产生一个正的跳变。代码通过判断差值是否超过阈值来识别并正确处理这些情况。
Motor_Update函数是PID控制的主循环。它首先计算当前速度,然后调用PID_Calculate函数得到控制输出,最后根据输出的正负来判断电机转向并更新PWM占空比。代码中设置了10的阈值来消除低速时的抖动,当PID输出绝对值小于10时直接停止电机。
5.4 主程序与中断处理
主程序负责系统的初始化和主循环的运行。
文件名:main.c
c
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "gpio.h"
#include "MotorPID.h"
/* Private variables ---------------------------------------------------------*/
// 定义左电机和右电机
Motor_TypeDef LeftMotor;
Motor_TypeDef RightMotor;
// 控制周期定时器标志位
volatile uint8_t control_flag = 0;
// 串口调试用
uint8_t rx_data;
volatile uint8_t uart_rx_complete = 0;
volatile uint8_t rx_buffer[32];
volatile uint8_t rx_index = 0;
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void Motor_GPIO_Init(void);
void Motor_Struct_Init(void);
/**
* 电机GPIO初始化配置
* 由CubeMX生成的代码修改
*/
void Motor_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
/* Configure Left Motor Direction Control Pins (PA4, PA5) */
GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* Configure Right Motor Direction Control Pins (PA6, PA7) */
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* Set all direction pins to low (stop) */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);
}
/**
* 电机数据结构初始化
*/
void Motor_Struct_Init(void) {
// 初始化左电机
LeftMotor.dir1_port = GPIOA;
LeftMotor.dir1_pin = GPIO_PIN_4; // IN1
LeftMotor.dir2_port = GPIOA;
LeftMotor.dir2_pin = GPIO_PIN_5; // IN2
LeftMotor.pwm_timer = &htim1;
LeftMotor.pwm_channel = TIM_CHANNEL_1; // PWM通道1
LeftMotor.encoder_timer = &htim3;
// 初始化右电机
RightMotor.dir1_port = GPIOA;
RightMotor.dir1_pin = GPIO_PIN_6; // IN3
RightMotor.dir2_port = GPIOA;
RightMotor.dir2_pin = GPIO_PIN_7; // IN4
RightMotor.pwm_timer = &htim1;
RightMotor.pwm_channel = TIM_CHANNEL_2; // PWM通道2
RightMotor.encoder_timer = &htim3;
}
/**
* 控制周期定时器回调函数
* 每10ms调用一次
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
control_flag = 1;
}
}
/**
* 串口接收完成回调函数
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
rx_buffer[rx_index++] = rx_data;
if (rx_data == '\n' || rx_index >= 32) {
uart_rx_complete = 1;
rx_index = 0;
}
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
/**
* 打印调试信息
*/
void Print_Debug_Info(void) {
char buffer[128];
// 打印左电机信息
sprintf(buffer, "L: Target=%.1f, Current=%.1f, PWM=%.0f\r\n",
LeftMotor.pid.target,
LeftMotor.speed,
LeftMotor.pid.output);
HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), 100);
// 打印右电机信息
sprintf(buffer, "R: Target=%.1f, Current=%.1f, PWM=%.0f\r\n",
RightMotor.pid.target,
RightMotor.speed,
RightMotor.pid.output);
HAL_UART_Transmit(&huart1, (uint8_t*)buffer, strlen(buffer), 100);
}
/**
* 解析串口命令
* 格式:L 100(设置左电机速度为100rpm)
* R 50(设置右电机速度为50rpm)
* S(停止所有电机)
*/
void Parse_Command(uint8_t* cmd) {
char cmd_type;
float cmd_value;
if (sscanf((char*)cmd, "%c %f", &cmd_type, &cmd_value) == 2) {
switch (cmd_type) {
case 'L':
case 'l':
Motor_Set_Speed(&LeftMotor, cmd_value);
break;
case 'R':
case 'r':
Motor_Set_Speed(&RightMotor, cmd_value);
break;
case 'B':
case 'b':
Motor_Set_Speed(&LeftMotor, cmd_value);
Motor_Set_Speed(&RightMotor, cmd_value);
break;
}
} else if (cmd[0] == 'S' || cmd[0] == 's') {
Motor_Stop(&LeftMotor);
Motor_Stop(&RightMotor);
} else if (cmd[0] == 'P' || cmd[0] == 'p') {
// PID参数调整:P Kp Ki Kd
float kp, ki, kd;
if (sscanf((char*)cmd + 1, "%f %f %f", &kp, &ki, &kd) == 3) {
LeftMotor.pid.Kp = kp;
LeftMotor.pid.Ki = ki;
LeftMotor.pid.Kd = kd;
RightMotor.pid.Kp = kp;
RightMotor.pid.Ki = ki;
RightMotor.pid.Kd = kd;
}
}
}
/**
* 主函数
*/
int main(void) {
/* MCU Configuration--------------------------------------------------------*/
HAL_Init();
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM1_Init();
MX_TIM2_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
/* 初始化电机控制相关GPIO */
Motor_GPIO_Init();
/* 初始化电机数据结构 */
Motor_Struct_Init();
/* 初始化电机 */
Motor_Init(&LeftMotor);
Motor_Init(&RightMotor);
/* 启动PWM输出 */
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 左电机PWM
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); // 右电机PWM
/* 启动控制周期定时器(10ms周期) */
HAL_TIM_Base_Start_IT(&htim2);
/* 启动串口接收 */
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
/* 打印启动信息 */
printf("\r\n========================================\r\n");
printf(" STM32 PID Motor Control System\r\n");
printf(" Version: 1.0\r\n");
printf("========================================\r\n");
printf("Commands:\r\n");
printf(" L <speed> - Set Left Motor Speed\r\n");
printf(" R <speed> - Set Right Motor Speed\r\n");
printf(" B <speed> - Set Both Motors Speed\r\n");
printf(" P <kp> <ki> <kd> - Set PID Parameters\r\n");
printf(" S - Stop All Motors\r\n");
printf("========================================\r\n\r\n");
/* 主循环 */
while (1) {
/* 处理控制周期(10ms) */
if (control_flag) {
control_flag = 0;
// 更新左电机
Motor_Update(&LeftMotor);
// 更新右电机
Motor_Update(&RightMotor);
}
/* 处理串口命令 */
if (uart_rx_complete) {
uart_rx_complete = 0;
rx_buffer[rx_index] = 0;
rx_index = 0;
// 解析并执行命令
Parse_Command((uint8_t*)rx_buffer);
}
/* 可以添加其他后台任务 */
}
}
/**
* System Clock Configuration
*/
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/* Initializes the RCC Oscillators according to the specified parameters
in the RCC_OscInitTypeDef structure. */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/* Initializes the CPU, AHB and APB buses clocks */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2 = 72MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
Error_Handler();
}
}
/**
* This function is executed in case of error occurrence.
*/
void Error_Handler(void) {
__disable_irq();
while (1) {
// 错误处理:可以让LED闪烁指示错误
}
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line) {
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
}
#endif /* USE_FULL_ASSERT */
主程序是整个系统的入口和调度中心。在main函数中,我们完成了所有外设的初始化,包括GPIO配置、定时器配置、串口配置等。然后初始化左右两个电机结构体,启动PWM输出和定时器中断。
系统使用TIM2作为控制周期定时器,每10ms产生一次中断。在中断回调函数中设置control_flag标志位,主循环检测到这个标志后就会执行PID控制计算。这种设计将中断处理与主循环分离,可以确保控制的实时性。
串口通信功能允许用户通过调试助手发送命令来控制电机和调整PID参数。支持的命令格式在代码注释中有详细说明。
六、系统调试与优化
6.1 调试方法与步骤
嵌入式系统的调试需要讲究方法和步骤,盲目调试往往会事倍功半。下面我们详细介绍本项目的调试流程。
第一步:硬件检查
在给系统上电之前,首先要仔细检查硬件连接。确认所有电源线连接正确,正负极没有接反。检查每一个信号连接,确保没有虚焊或接触不良。特别注意编码器的接线,编码器的VCC和GND不能接反,否则会损坏编码器。
使用万用表测量各点电压:12V电源输入端应该有稳定的12V左右电压;L298N的+5V输出端应该有4.5V-5.5V的电压;STM32的3.3V供电引脚应该有3.3V左右电压。
第二步:基础功能测试
给系统上电后,首先测试基础功能是否正常。LED指示灯应该正常点亮,说明系统已经上电运行。
使用示波器或逻辑分析仪检查PWM输出。将示波器探头连接到PA0引脚,应该能看到频率为36KHz的PWM波形。调节PWM占空比,波形的占空比应该相应变化。
手动转动电机轴,检查编码器输出。使用示波器观察PC6和PC7引脚,应该能看到两相正交脉冲波形。两相波形应该有90度的相位差。
第三步:PID参数整定
完成基础功能测试后,就可以开始PID参数整定了。这是让系统稳定运行的关键步骤。
建议先从比例环节开始调试。将Ki和Kd都设为0,逐步增大Kp,观察电机的响应。理想情况下,电机会快速响应速度变化,但不应该出现持续的振荡。如果电机转速超过目标值后开始来回摆动,说明Kp太大了,需要减小。
确定合适的Kp后,逐步增大Ki来消除稳态误差。观察电机是否能够稳定在目标速度,同时响应速度变化是否令人满意。如果电机出现振荡,可能是Ki太大了。
最后加入微分环节Kd。微分的加入应该能够抑制超调,使系统更快稳定。从较小的值开始,逐步增加,直到系统达到满意的动态性能。
第四步:性能测试
PID参数整定完成后,需要对系统进行全面测试。测试不同的目标速度,检查系统在各种速度下的稳态误差。测试速度突变时系统的响应,观察是否有超调和振荡。测试加载后的表现,可以在电机轴上施加一定的负载,观察速度是否能够快速恢复。
6.2 常见问题与解决方法
在调试过程中可能会遇到各种问题,下面列出常见问题及其解决方法。
问题一:电机不转
如果电机完全没有反应,首先检查电源是否正常供电。测量L298N的电源输入端是否有12V电压,检查输出端是否有电压。使用万用表检查方向控制引脚的电平是否正确设置。
检查PWM信号是否正常。用示波器观察PWM引脚,确认有PWM波形输出。检查L298N的ENA引脚是否通过跳线连接到了+5V,如果没有跳线,需要将ENA直接连接到5V或者PWM信号。
检查电机本身是否损坏。可以直接将电机连接到电源上(不通过L298N),看电机是否能转动。
问题二:电机只向一个方向转
如果电机只能单向转动,首先检查两个方向控制引脚的接线是否正确。检查程序中方向控制的逻辑是否正确,特别注意正反转的GPIO设置是否与硬件连接匹配。
检查编码器接线是否正确。虽然编码器接线错误不应该影响方向控制,但如果A相和B相接反了,会导致速度计算为负值,这可能影响PID控制器的输出。
问题三:速度波动大
如果电机转速波动很大,可能是PID参数设置不合理。尝试减小Kp和Ki,增加Kd。检查编码器信号是否有干扰,可以在编码器电源引脚并联滤波电容。检查PWM频率是否合适,频率太低会导致电机运行不平稳,频率太高可能导致L298N无法正确响应。
问题四:电机启动缓慢
如果电机启动很慢,响应不灵敏,可能是积分项太小或者比例系数太小。可以适当增大Kp和Ki的值。也可能是目标速度设置得太高,超过了电机的能力范围。
问题五:编码器计数不准
如果编码器计数值不准确,首先检查编码器电源是否稳定。检查STM32的编码器模式配置是否正确,确保使用了四倍频模式。检查PC6和PC7的GPIO配置,确保使用了正确的复用功能。
6.3 性能优化技巧
在完成基本功能后,可以进一步优化系统性能。
优化一:改变控制周期
控制周期对系统性能有重要影响。控制周期太短会导致CPU负载过高,可能影响其他任务的执行;控制周期太长会导致系统响应变慢,控制效果变差。建议将控制周期设置在5ms-20ms范围内,根据实际情况调整。
优化二:加入前馈控制
对于电机调速系统,可以加入速度前馈来提高响应速度。前馈控制根据目标速度直接给出一个基础PWM值,PID控制器只需要补偿前馈无法覆盖的部分。这样可以大大加快系统的响应速度,同时保持较好的稳态精度。
前馈值的计算方法:在已知电机模型的情况下,可以计算出给定速度所需的PWM占空比。简化处理的话,可以直接使用目标速度的一定比例作为前馈值。
优化三:加入加速度限制
为了防止速度突变对机械系统造成冲击,可以加入加速度限制功能。在设置目标速度时,不是直接设置,而是让实际目标速度逐步逼近设定的目标速度。这样可以使电机启动和停止更加平缓,减少机械磨损。
优化四:加入滤波算法
编码器信号可能会受到电磁干扰,可以在速度计算后加入滤波算法。常用的滤波方法有滑动平均滤波、卡尔曼滤波等。滑动平均滤波实现简单,效果也不错。卡尔曼滤波效果更好,但实现也更复杂。
性能优化
优化控制周期
加入前馈控制
加入加速度限制
加入滤波算法
问题诊断
电机不转
检查电源
检查PWM信号
检查方向控制
速度波动大
调整PID参数
检查编码器信号
增加滤波算法
响应太慢
增大Kp Ki
加入前馈控制
调试流程
硬件检查
基础功能测试
PID参数整定
性能测试
七、扩展应用与总结
7.1 实际应用场景
基于本项目实现的直流电机PID闭环调速系统,可以应用于多种实际场景。
智能小车底盘控制
最典型的应用就是智能小车的底盘控制。左右两个电机分别控制小车的左轮和右轮,通过独立调速可以实现小车的直行、转弯、掉头等运动。PID控制器可以确保小车在不同地面上都能保持稳定的速度行驶。如果配合陀螺仪传感器,还可以实现循迹行驶和平衡控制。
传送带速度控制
在工业自动化生产线上,传送带的速度控制非常重要。通过PID闭环控制,可以确保传送带在不同负载下都能保持恒定的运行速度。这对于保证产品质量和提高生产效率都有重要意义。
云台稳定系统
在无人机云台或相机稳定器中,需要保持摄像头始终水平或指向特定方向。通过PID控制电机来抵消外部干扰(如风力、震动等),可以实现稳定的图像输出。
机械臂关节控制
在机械臂的各个关节中,电机控制精度直接影响机械臂的定位精度。使用PID闭环控制可以精确控制每个关节的角度,实现高精度的位置控制。
7.2 系统升级建议
如果希望进一步提升系统性能,可以考虑以下升级方向。
升级一:使用更高性能的控制器
STM32F103虽然功能强大,但如果需要更复杂的控制算法或多任务处理,可以考虑使用STM32F4或STM32H7系列,它们有更高的主频和更多的外设资源。
升级二:增加通信接口
可以增加蓝牙模块或WiFi模块,实现无线控制。这样就可以用手机APP或电脑软件来控制电机,甚至可以实现远程监控和调试。
升级三:增加传感器融合
可以加入陀螺仪、加速度计等传感器,实现多传感器融合。这样可以获得更准确的状态估计,提高控制效果。
升级四:实现高级控制算法
除了PID,还可以尝试实现模糊控制、神经网络控制、自适应控制等高级算法。这些算法在处理非线性系统和不确定系统时有更好的效果。
7.3 项目总结
通过本项目的学习,我们完整地掌握了一个嵌入式闭环控制系统的开发流程。
在硬件层面,我们学习了如何选择和连接电机驱动模块、编码器和主控芯片。理解了L298N电机驱动的工作原理,掌握了光电编码器输出信号的读取方法。
在软件层面,我们深入学习了PID控制算法的数学原理和程序实现。从增量式PID的公式推导到代码实现,从参数整定方法到调试技巧都有了全面的掌握。
在系统设计层面,我们理解了闭环控制系统的整体架构,学会了将理论知识转化为实际可运行的系统。通过本项目,我们不仅掌握了嵌入式开发的基本技能,还培养了解决实际工程问题的能力。
嵌入式开发是一个需要不断实践和积累的过程。希望大家能够以本项目为基础,继续探索更高级的控制算法和更复杂的嵌入式系统。祝大家在嵌入式开发的道路上越走越远!
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 项目知识体系总结 │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 硬件知识 │ │
│ │ ├── STM32F103C8T6 单片机 │ │
│ │ ├── L298N 电机驱动模块 │ │
│ │ ├── 直流减速电机带光电编码器 │ │
│ │ └── 电源系统设计 │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 软件知识 │ │
│ │ ├── PID 控制算法(比例、积分、微分) │ │
│ │ ├── 增量式 PID 实现 │ │
│ │ ├── PWM 脉宽调制 │ │
│ │ ├── 编码器脉冲捕获 │ │
│ │ └── 定时器中断处理 │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 开发技能 │ │
│ │ ├── STM32CubeMX 配置 │ │
│ │ ├── Keil MDK 编程调试 │ │
│ │ ├── 串口调试通信 │ │
│ │ └── PID 参数整定 │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 应用拓展 │ │
│ │ ├── 智能小车 │ │
│ │ ├── 传送带控制 │ │
│ │ ├── 云台稳定系统 │ │
│ │ └── 机械臂控制 │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────┘