STM32嵌入式开发:基于PID算法的直流电机闭环调速控制

文章目录

    • 一、项目概述与学习目标
      • [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控制算法原理
    • 四、开发环境搭建
      • [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 参数整定                                                            │   │
│  └─────────────────────────────────────────────────────────────────────────────────┘   │
│                                                                                         │
│  ┌─────────────────────────────────────────────────────────────────────────────────┐   │
│  │                              应用拓展                                            │   │
│  │  ├── 智能小车                                                                  │   │
│  │  ├── 传送带控制                                                               │   │
│  │  ├── 云台稳定系统                                                            │   │
│  │  └── 机械臂控制                                                               │   │
│  └─────────────────────────────────────────────────────────────────────────────────┘   │
│                                                                                         │
└─────────────────────────────────────────────────────────────────────────────────────────┘

相关推荐
weixin_6495556743 分钟前
C语言程序结构第四版(何钦铭、颜晖)第十章函数与程序结构之递归实现顺序输出整数
c语言·数据结构·算法
jghhh0143 分钟前
51单片机控制42步进电机程序
单片机·嵌入式硬件·51单片机
想七想八不如114081 小时前
复试简历复盘--CV论文
算法
cm6543201 小时前
C++中的空对象模式
开发语言·c++·算法
2401_851272991 小时前
C++代码规范化工具
开发语言·c++·算法
Yzzz-F1 小时前
Problem - 2167F - Codeforces
算法
MORE_771 小时前
leecode100-跳跃游戏-贪心算法
算法·游戏·贪心算法
机器学习之心1 小时前
基于GSWOA-SVM三种策略改进鲸鱼算法优化支持向量机的数据多变量时间序列预测,Matlab代码
算法·支持向量机·matlab·优化支持向量机·gswoa-svm·三种策略改进鲸鱼算法
旖-旎1 小时前
前缀和(和为K的子数组)(5)
c++·算法·leetcode·前缀和·哈希算法·散列表
zmj3203241 小时前
PLC与单片机(微控制器MCU)、传统继电器控制系统
单片机·嵌入式硬件·plc