本文将详细介绍如何使用 STM32F103C8T6 微控制器,通过 TB6612 双电机驱动芯片驱动 12V 直流编码电机。我们将采用标准 PWM 调速方式,并利用 PID 控制算法实现电机转速和位置的闭环控制。教程内容从基础原理开始,逐步涵盖硬件连接、开发环境配置、驱动代码实现、PID 控制算法以及完整实例代码,最后提供调试与优化的建议。即使是零基础的读者,通过本教程也能逐步掌握相关知识和实现方法。
1. 基础知识
在开始实际操作之前,需要了解一些基础理论,包括电机驱动芯片 TB6612 的工作原理、编码电机(带霍尔传感器)的工作方式、PWM 调速原理,以及 PID 控制在闭环控制中的作用。
TB6612 驱动芯片工作原理
TB6612FNG(简称 TB6612)是一款常用的双通道直流电机驱动芯片,内部集成了两路 H 桥驱动器。与传统的 L298N 不同,TB6612 采用 MOSFET 输出,具有更低的导通电阻和更高的效率,可提供平均1.2A(峰值3.2A)的电流 (TB6612FNG)。TB6612 支持双通道控制,可以同时驱动两台直流电机。
TB6612 的引脚包括电源引脚、控制引脚和输出引脚三类 (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)。其中重要的控制引脚有:
- PWMA/PWMB: 用于控制电机A和电机B速度的PWM输入引脚。
- AIN1/AIN2 和 BIN1/BIN2: 分别用于控制电机A和电机B转动方向的逻辑输入引脚。
- STBY: 芯片使能(待机)引脚,需置为高电平才能正常工作。
- AO1/AO2 和 BO1/BO2: 电机A和电机B的输出端口,连接到电机两端。
- VM 和 VCC: VM为电机驱动电压输入(可达15V),VCC为逻辑电压输入(2.7~5.5V) (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)。
TB6612 的工作模式由 AIN1/AIN2 (或 BIN1/BIN2) 和 PWM 引脚共同决定 (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)。在 STBY 高电平(芯片使能)的情况下,控制逻辑如下:
- 当提供 PWM 信号且两个方向引脚一高一低时,电机按对应方向转动(CW 或 CCW)。例如对于电机A: AIN1=1、AIN2=0 时正转,AIN1=0、AIN2=1 时反转 (HAL库直流有刷电机的PWM驱动以及PID控制算法的实现_pid控制pwm来控制电机代码-CSDN博客)。
- 当两个方向引脚都为低电平时,电机处于制动(快速刹车)模式,此时如果PWM为高,H桥会短接电机两端使其快速停止;如果 PWM 为0则电机停转且自由滑行。
- 当两个方向引脚都为高电平时,TB6612 也会使电机刹车(与两个为低类似)。
- 要让电机停止且自由滑行(不刹车),可以将 PWM 信号置0占空比,同时将方向引脚任意设定,这样实际没有驱动输出,相当于关闭输出使电机惯性滑行。
简单来说,PWM 引脚控制电机速度,AIN1/AIN2 控制电机转动方向。只有在 STBY=HIGH 且 PWM 有输出的情况下,方向引脚的组合才驱动电机 (HAL库直流有刷电机的PWM驱动以及PID控制算法的实现_pid控制pwm来控制电机代码-CSDN博客)。注意: TB6612 的逻辑电源 VCC 要求2.7~5.5V,确保与 STM32F103C8T6 的3.3V逻辑兼容。同时所有地线需要共地。
编码电机的工作原理(霍尔传感器测速)
编码电机是指带有位置/速度传感器的直流电机。常见的编码器包括光电编码器和霍尔效应编码器。这里我们以 霍尔传感器编码器 为例说明工作原理。
霍尔编码器通常在电机轴上安装一个带有磁铁的轮盘,旁边布置霍尔传感器。当电机轴旋转时,磁铁随之转动,每经过霍尔传感器一次就会触发传感器输出一个脉冲信号 (Hall Effect Sensor and Its Role in a Motor Controller - Embitel)。典型的单霍尔传感器会输出一连串脉冲,频率与电机转速成正比。某些电机有两个霍尔传感器输出相位相差90度的脉冲(这构成增量式编码器的A/B两路信号),通过这两路信号可以判定转动方向并更精准地计数脉冲。对于初学者,如果电机只提供一路霍尔信号,我们仍可通过记录脉冲数量来得到速度,并利用控制信号方向来推断电机的转动方向。
通过霍尔传感器测速的基本方法有两种:
- 计数法:在固定时间窗口内统计收到的脉冲个数,据此计算转速。例如1秒内收到N个脉冲,若编码器每转一圈产生P个脉冲,那么电机每秒转了 N/P 圈,即 RPM = (N/P)*60。
- 周期法:测量相邻脉冲间的时间间隔,然后计算频率。频率f(每秒脉冲数)与转速的关系: 转速(转每秒) = f / P, 转速(RPM) = (f / P) * 60。对于高速或高分辨率编码器,用周期法可以更快地感知速度变化,而计数法在低速时精度更高些。
编码器不仅可用于测速,也可用于测位置。通过累计脉冲数,我们可以知道电机轴旋转了多少圈或多少角度。例如,如果编码器每圈输出P个脉冲,那么累计计数达到P就代表轴转了一整圈。结合转向信息(由A/B两路或由控制指令知道),可以增加或减少计数,实现对相对位置的检测。如果需要绝对位置,通常要有参考零点或者使用更复杂的绝对编码器。本教程的电机位置控制基于相对脉冲计数,即通过设定脉冲目标值来控制电机转到目标位置。
PWM 控制电机速度的原理
PWM(Pulse Width Modulation,脉宽调制)是一种控制电机等执行器的常用技术。PWM 信号本质上是周期性的方波,通过调节方波的占空比(High 电平时间与总周期的比例)来调节功率输出。 (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)指出,PWM 调制改变占空比,相当于快速切换驱动的开关状态,使负载(电机)获得不同的平均电压,从而改变电机速度。
对于直流电机而言:
- 当PWM占空比为0%时,相当于一直断开电源,电机不转动。
- 当占空比为50%时,意味着电机一半时间通电、一半时间断电,电机获得约一半的平均电压,转速降低。
- 当占空比为100%时,相当于一直通电(直流电压全加),电机以最大速度运转。
通过固定频率、可变占空比的PWM,我们可以线性地控制电机速度 (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)。频率一般选择较高值(几千Hz以上)以避免电机产生可闻噪音和降低电流脉动。STM32 定时器可以方便地产生PWM波形,微控制器只需调整占空比(比较值),就能实时改变电机速度。
PID 控制在电机速度/位置闭环中的作用
开环控制(不使用反馈)时,我们仅设置PWM占空比让电机转动,无法确保实际达到期望的速度或位置。例如,负载变化或电压波动会导致电机转速偏离设定值。而闭环控制(反馈控制)通过传感器获取实际速度/位置,并根据期望与实际的误差不断调整驱动输入,从而使电机输出跟随设定目标。
PID 控制器是一种常用且高效的闭环控制算法,由比例 (P)、积分 (I)、微分 (D) 三部分组成:
- 比例 (P): 根据当前误差乘以比例系数调整输出。P 控制能快速响应误差,但单独使用时可能存在稳态误差(无法完全消除偏差)。
- 积分 (I): 积累误差并根据累计量调整输出,可以消除稳态误差。但积分过大会导致系统反应滞后,出现超调或振荡(积分饱和问题)。
- 微分 (D): 根据误差变化率调整输出,有预见性地减小误差趋势。D 控制可以提高系统稳定性,减小超调,但对噪声敏感。
在电机速度控制中,PID 控制器根据“期望转速”和“实际转速”计算误差,不断调整PWM占空比,使实际转速逼近期望值,实现恒速控制。在位置控制中,PID 控制器根据“目标位置”(目标脉冲数)和“当前累计脉冲数”计算误差,调整电机转动方向和速度,使电机移动到目标位置并稳定在那里。
PID 控制属于闭环控制的核心部分,它的引入使得系统能自动补偿外界干扰和参数变化。例如,当上坡导致电机变慢时,速度误差增大,PID 控制会自动提高PWM占空比增大扭矩;在位置控制中,当接近目标位置时误差减小,PID 会降低驱动力度防止冲过头。合理调整 PID 三个参数 (Kp、Ki、Kd) 可以使电机实现快速响应、稳准跟踪、无静差的控制效果。
延伸: PID 控制算法有两种常见实现形式——位置式和增量式。位置式PID直接计算控制量的绝对值,而增量式PID计算控制量的增量,优点是对计算误差累积不敏感。后文将对这两种实现有所提及。
2. 硬件连接
了解原理后,我们需要将 STM32 微控制器、TB6612 驱动板、编码电机和电源正确连接。本节介绍电机驱动和编码器信号的接线方法,以及12V供电的注意事项。
TB6612 与 STM32F103C8T6 的接线方式
首先确定使用 TB6612 的哪个通道来驱动电机。如果只驱动一台电机,我们可以使用 TB6612 的通道A(对应 AIN1, AIN2, PWMA 等引脚)。连接方法如下:
-
电源连接:
- 将 TB6612 的 VM 引脚连接到直流12V电源的正极;GND 引脚连接电源地。注意: 确保 STM32 和电机驱动共地(GND 相连),否则信号无法正确参考。
- TB6612 的 VCC 引脚连接到 STM32F103C8T6 的 3.3V 输出(或者5V也可,但用3.3V可保证逻辑电平兼容)。根据 TB6612 数据手册,VCC 范围2.7~5.5V (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed),所以3.3V足以驱动其内部逻辑。
- STBY 引脚为芯片使能,引脚拉高使能输出。可将 STBY 直接连接到 VCC(3.3V)使其始终使能,或连接到STM32的一个GPIO以便软件控制芯片待机/唤醒(简单起见可直接接3.3V)。
- 在电源两端(靠近TB6612模块)建议接入适当的去耦电容(如 VM 与地之间 10µF + 0.1µF)以滤除电机电刷噪声,稳定供电 (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)。
-
电机连接:
- 将直流电机的两个引线分别连接到 TB6612 的 AO1 和 AO2 引脚(A通道输出)。电机引线无极性区分,只是正反转方向相对变化。
- 如果TB6612模块上有引出 AO1、AO2 接线端子,则将电机连接到相应端子即可。
-
控制信号连接:
- 选择 STM32F103C8T6 上支持 PWM 输出的一个通用定时器通道,引脚接到 TB6612 的 PWMA 引脚。STM32F103C8T6 有多个定时器,例如 TIM2~TIM4,每个有若干通道支持 PWM 输出。后续我们将通过 CubeMX 选择具体的引脚和定时器。例如,可以使用 TIM3_CH1 (对应引脚 PA6) 或 TIM2_CH1 (PA0) 等作为 PWM 输出。
- 选择两个通用数字输出引脚,接到 TB6612 的 AIN1 和 AIN2 引脚,用于控制电机方向。任意空闲的 GPIO 都可以,例如将 PA1 接 AIN1,PA2 接 AIN2。注意这些引脚在 CubeMX 中配置为输出模式。
- 如果需要控制 TB6612 的 STBY(非一直接高),也选择一个GPIO连接 STBY,并确保上电时软件将其拉高,才可驱动电机。
连接完成后,STM32 就可以通过PWM引脚输出的占空比控制转速,通过AIN1/AIN2的电平组合控制转动方向或刹车。务必检查共地,以及所有控制引脚电压均不超过TB6612 VCC电压。
编码器霍尔信号与 STM32 连接
编码电机上通常引出编码器的信号线。以常见的双霍尔(两相 AB 相位)编码器为例,会有A相、B相两根信号线(还有电源供电线,一般为 Vcc 和 GND)。若是单霍尔传感器,则只有一根脉冲输出线和电源线。典型的霍尔传感器工作电压为5V或3.3V,如果编码器模块标称5V且输出也是5V电平,直接接STM32(3.3V逻辑)需要注意电平兼容,可通过电阻分压或逻辑电平转换。很多情况下霍尔传感器输出为开漏集电极,需要上拉电阻,可利用STM32的内部上拉或外接上拉到3.3V来获得可靠的3.3V脉冲信号。
将编码器信号连接 STM32 的方法:
- 单通道测速:将编码器的脉冲输出连接到 STM32 的一个定时器输入捕获通道或外部中断引脚。例如,使用 TIM4_CH1 (对应引脚 PA0) 配置为输入捕获;或者使用 PA0 的外部中断通道 EXTI0。如果编码器有两路信号但初学阶段只需要测速,也可以暂时只接A相信号进行测速。
- 双通道计数:如果需要知道方向并精确计数位置,可将编码器的A相、B相分别接到STM32一个定时器的编码器接口模式支持的两路输入。例如 STM32F103 的 TIM2、TIM3、TIM4 支持编码器模式,将 CH1, CH2 分别接A相和B相。CubeMX 可以将某个定时器设置为 “Encoder Mode”,硬件自动对脉冲进行计数增减。但编码器模式较复杂,这里主要介绍单通道捕获测速的方法,并通过代码逻辑处理方向/位置。
- 将编码器的地线接STM32地,编码器的电源线接5V或3.3V(根据编码器规范)。确保编码器输出的高电平不超过STM32的允许范围。如果编码器必须由5V供电且输出5V,高电平需通过硬件降到3.3V再接入(可用电阻分压,如 10k/10k 将5V分压为2.5V左右,或用芯片转换)。
如果使用定时器捕获模式读取速度:选用STM32的一个定时器通道作为输入捕获。在硬件上,将该定时器通道对应的引脚连接到编码器脉冲输出。例如 TIM2_CH1 (PA0) 或 TIM3_CH1 (PA6) 等。该引脚在 CubeMX 中配置为 “GPIO_Input” 并开启 “TIMx CHy Input Capture” 功能。硬件连接完成后,我们可以在软件中捕获脉冲的时间间隔或频率,进而算出转速。
12V 电源管理
由于电机使用12V供电,而且电机启动瞬间和堵转时电流较大(可能几百毫安到数安培,视电机而定),电源部分需要注意以下几点:
- 电源容量: 使用能够提供足够电流的直流电源。若使用电池,要确保电池输出稳定且有足够放电能力;若用电源适配器,建议选择比电机空载电流高出数倍容量的适配器。
- 降压稳压: STM32F103C8T6 工作在3.3V电压,TB6612的逻辑也需要3.3~5V电压。因此需要一个将12V降压为稳定5V和3.3V的电路。常用方案是在12V输入后使用一块 DC-DC 降压模块(或7805等稳压器)得到5V,再通过 AMS1117-3.3 等线性稳压得到3.3V**(如果没有其他3.3V源)**。实际应用中,“蓝色小板 (BluePill)” STM32开发板上已经有USB供电转3.3V的稳压芯片,但12V不能直接给开发板,需要外部先降到5V输入板子的5V脚或直接提供3.3V。
- 电源隔离与保护: 电机在切换和停止时会产生反向电动势冲击。TB6612 内部已集成二极管处理反冲,但仍建议在电机两端跨接一个小的电容来滤波(如0.1µF陶瓷电容)。此外,可以在TB6612的VM引脚附近加入一个TVS二极管以吸收瞬态过压(视情况而定)。确保电源线路尽可能短粗,减少线路压降。
- 地线连接: 强调所有设备共地:电源地、TB6612地、STM32地、编码器地要连接在一起形成公共地。否则信号基准不同,会导致测量和控制异常。
完成以上硬件连接与电源准备后,整个系统的结构如下:STM32通过PWM和方向引脚控制TB6612,TB6612驱动电机;编码器霍尔传感器将电机转动反馈给STM32输入引脚;12V电源为电机提供动力并通过降压为控制电路供电。确认连接无误后即可进行软件配置和编程。
3. 开发环境配置
在开始编写代码前,我们需要搭建 STM32 的开发环境并进行初步配置。我们将使用 STM32CubeIDE(集成了 CubeMX 图形配置和IDE)来创建工程,通过 CubeMX 配置 PWM 和定时器输入捕获(或外部中断)等外设,从而生成基础初始化代码。
STM32CubeIDE 项目创建
- 安装 STM32CubeIDE: 如果尚未安装,请从 ST 官方网站下载 STM32CubeIDE 并安装。该工具集成了工程管理、代码编辑、调试和 CubeMX 配置功能,适合本案例使用。
- 新建工程: 打开 STM32CubeIDE,选择 "File -> New -> STM32 Project"。在芯片/板卡列表中找到 STM32F103C8T6(如果使用的是“Blue Pill”开发板,可以直接选择芯片 STM32F103C8Tx)。点击 Next。
- 选择项目模板: 选择 “CMSIS/Core” 或 “STM32Cube” 默认初始化项目,确保勾选 “Initialize all peripherals with their default Mode” 以使用 CubeMX 初始化外设。填写工程名称和保存位置,完成向导。
- 打开 CubeMX 配置: 工程创建完成后,会自动打开 CubeMX 图形化配置界面(.ioc 文件)。我们将在其中配置时钟和所需的GPIO/定时器外设。首先,在 "Pinout & Configuration" 视图下,配置系统时钟为合适频率(比如开启 HSE 外部晶振8MHz并启用 PLL,使SYSCLK=72MHz,这是 STM32F103C8 的最高主频)。
- 时钟树: 在 “RCC” 中选择 HSE 和 PLL的设置。CubeMX 一般能自动计算 PLL参数以达到72MHz。
- 保存: 在进行具体外设配置前,先保存工程以应用时钟配置。
CubeMX 配置 PWM 输出控制 TB6612
接下来配置定时器PWM输出,以产生控制 TB6612 所需的 PWM 信号:
- 选择定时器: 打开 CubeMX 的 Pinout 界面下的 “Timers” 列表。在 STM32F103C8T6 上,有通用定时器 TIM2~TIM4 以及高级定时器 TIM1 可用于 PWM。选择一个空闲的定时器,比如 TIM3,用于产生PWM。
- 开启 PWM 通道: 展开 TIM3 配置,将 Channel1(假设用通道1)模式设置为 “PWM Generation CH1”。这样 TIM3_CH1 就被启用为PWM输出模式。
- 引脚映射: 当启用 TIM3_CH1 后,CubeMX 左侧的引脚视图会高亮对应引脚(STM32F103C8T6 的 TIM3_CH1 默认映射在 PA6)。点击该引脚可以看到功能已被分配为 TIM3_CH1。我们可以保留这个引脚用于 PWM 输出,并将其实际连接到 TB6612 的 PWMA 引脚(在硬件连接中已确定)。
- PWM参数: 切换到 Configuration 标签,找到 TIM3 的参数设置。设置 Prescaler 和 Counter Period 以得到合适的 PWM 频率和分辨率。例如,我们希望 PWM 频率为 10 kHz 左右。假设 APB1 定时器时钟频率为 72 MHz(因为TIM3挂在APB1,APB1默认分频2倍给定时器,实际TIM时钟可能是 36MHz或72MHz,需留意),我们可以设置 Prescaler 和 Period 满足:
PWM_frequency = TimerClk / ((Prescaler+1)*(Period+1))
。例如,Prescaler=71,Period=99,将得到约10kHz的PWM(72MHz/(72*100) = 10kHz)。注意: 实际计算需根据 Timer 时钟计算。CubeMX 也提供直接填写期望频率的选项(在 PWM 设置中可填写 Desired frequency,然后调整Period/Prescaler)。 - PWM极性和模式: 一般默认配置下,PWM极性为高电平有效(High True),即占空比对应高电平时间比例。这符合我们的需求。确保 PWM 模式选择为 PWM mode 1(对应比较值越高输出高电平时间越长)。
- 使能定时器中断(可选): 对于PWM输出,我们通常不需要中断。但如果希望在定时器更新时做某些处理,可以在 NVIC 设置里启用 TIM3 更新中断。本例暂不启用,因为PWM不需要软件实时干预。
配置完成后,TIM3_CH1 将输出 PWM 波形。稍后我们会在代码中调用 HAL 库函数启动 PWM 输出,并根据 PID 计算结果修改占空比。
CubeMX 配置编码器脉冲输入 (定时器捕获/外部中断)
为了获取编码器的反馈信号,我们需要配置 STM32 的输入捕获或外部中断。这里介绍使用定时器输入捕获测量编码器脉冲周期的方法:
- 选择定时器: 在 CubeMX 的 “Timers” 中,选择另一个定时器用于捕获输入信号。比如使用 TIM2 作为输入捕获定时器。TIM2 有通道1-4,其中 CH1 可以映射到 PA0(也就是 Encoder A相我们接入的引脚)。
- 配置输入捕获: 将 TIM2 的 Channel1 模式设置为 “Input Capture direct mode”。这样 TIM2_CH1 被用作输入捕获输入。
- 参数设置: 在 Configuration 选项卡中,找到 TIM2。将 Channel1 的捕获极性设置为 “Rising Edge” (假设我们只在上升沿捕获脉冲)。也可选择 Both Edges 如果需要捕获更高分辨率频率(每个脉冲的上升和下降都算),但一般上升沿即可。
- 定时器频率: 为了测量脉冲周期,需要让 TIM2 自由运行计数作为时间基准。设置 TIM2 Prescaler 使得计数频率足够高以提供时间精度。例如,如果使用 72MHz 时钟,可以设置 Prescaler 为 71(计数频率1MHz,每计数=1微秒)。然后设置 TIM2 的 Period 为 0xFFFF(65535)即最大,确保计数器足够长时间不溢出(1MHz下65535计数约0.0655秒,对于一般电机脉冲周期足够)。
- 启用中断: 勾选 TIM2 的 “Capture Compare 1 interrupt” 使能捕获通道1中断。在 NVIC 设置中使能 TIM2 全局中断。这将使得每当捕获到上升沿时,产生中断,进入捕获回调以处理。
- GPIO设置: 确认 PA0 引脚的模式被自动配置为 “Alternate Function Input” (或“GPIO Input”) 且映射到 TIM2_CH1 功能。CubeMX 通常会自动处理。当 TIM2_CH1 设置为输入捕获后,PA0 引脚在 Pinout 图上应显示 "TIM2_CH1".
如果采用外部中断计数方法(不测周期而是在固定时间内计数):可以在 CubeMX 中不使用定时器捕获,而改为:
- 在 GPIO 视图中,将连接编码器脉冲的引脚(如 PA0)模式设为 "GPIO_EXTI0" (外部中断输入).
- 在 NVIC 中使能 EXTI0 中断。
- 这样每次该引脚有信号边沿时(上升沿或下降沿,由GPIO设置决定,默认上升沿),STM32会触发中断,软件中在回调里自增计数。之后可以用另一个定时器或 systick定时每隔一定周期读取计数计算速度。
- CubeMX 对 EXTI 上升沿/下降沿的配置在 GPIO 设置里(选择 Trigger),可选 Rising/Falling/Both。一般选择 Rising Edge 触发即可。
本教程以定时器输入捕获方案为例,因为它可以直接测量脉冲间隔,方便计算即时速度。但初学者如觉得复杂,也可以用 EXTΙ 方法,通过周期定时读取累计脉冲数计算平均速度。
- 保存并生成代码: 完成以上配置后,点击上方 “GENERATE CODE” 生成代码。CubeIDE 会生成初始化代码,包括 MX_GPIO_Init(), MX_TIM3_Init(), MX_TIM2_Init() 等函数,在 main() 中调用。这为我们后续编写控制逻辑打下基础。
4. 电机驱动代码实现
当工程初始化代码生成后,我们需要编写控制电机和读取编码器的代码。本节将介绍 PWM 信号初始化与控制、电机正反转和停止的实现、编码器脉冲的读取方法以及转速计算方法。
PWM 信号初始化与启动
CubeMX 已生成 PWM 定时器的配置代码,但我们仍需在 main.c 的合适位置启动PWM输出,并设置初始占空比。通常,在 main()
函数中初始化所有外设后(MX_TIMx_Init 调用之后),启动PWM输出。例如,如果我们使用 TIM3 通道1 来输出PWM:
// 假设 CubeMX 生成了 TIM3 句柄 htim3,并配置了通道1为PWM
MX_TIM3_Init(); // 初始化TIM3
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 开启TIM3通道1的PWM输出// 初始占空比设置为0(电机停转)
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
以上代码通常放在 /* USER CODE BEGIN 2 */
区域内,确保在进入 while(1)主循环前执行。HAL_TIM_PWM_Start
用于启动PWM输出通道,__HAL_TIM_SET_COMPARE
宏用于设置对应通道的比较值(占空比)。占空比的数值范围取决于TIM3定时器初始化时设置的Period值。例如我们Period设为99,则比较值099对应0%100%的占空比。如果Period=999,则0999对应0100%。
提示:使用
HAL_TIM_PWM_Start()
前要保证 PWM GPIO 已配置成复用输出模式(CubeMX 已做好),且定时器已经初始化。CubeMX 生成的 MX_TIM3_Init() 内部会调用 HAL_TIM_PWM_Init() 配置时基和通道参数,所以只需调用 Start 开始输出即可。
电机正转、反转、停止控制
通过控制 TB6612 的 AIN1, AIN2 引脚电平组合,可以实现电机方向控制和刹车。我们在 CubeMX 中将 AIN1, AIN2 配置为了 GPIO输出(推挽输出),并在 MX_GPIO_Init() 中进行了初始化(通常默认状态可以设为低)。接下来可以封装几个函数便于控制:
// 假设 AIN1 -> PA1, AIN2 -> PA2
#define AIN1_GPIO GPIOA
#define AIN1_PIN GPIO_PIN_1
#define AIN2_GPIO GPIOA
#define AIN2_PIN GPIO_PIN_2// 设置TB6612使能引脚(若接到GPIO):
#define STBY_GPIO GPIOA
#define STBY_PIN GPIO_PIN_3 // 假设PA3连接STBY,如果STBY直连3.3V则不需要此部分void Motor_Stop(void) {// 电机快速刹车: AIN1=0, AIN2=0, PWM占空比设为0以避免电流HAL_GPIO_WritePin(AIN1_GPIO, AIN1_PIN, GPIO_PIN_RESET);HAL_GPIO_WritePin(AIN2_GPIO, AIN2_PIN, GPIO_PIN_RESET);__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); // PWM占空比=0
}void Motor_Forward(uint16_t pwmDuty) {// 电机正转: AIN1=1, AIN2=0HAL_GPIO_WritePin(AIN1_GPIO, AIN1_PIN, GPIO_PIN_SET);HAL_GPIO_WritePin(AIN2_GPIO, AIN2_PIN, GPIO_PIN_RESET);__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwmDuty);
}void Motor_Backward(uint16_t pwmDuty) {// 电机反转: AIN1=0, AIN2=1HAL_GPIO_WritePin(AIN1_GPIO, AIN1_PIN, GPIO_PIN_RESET);HAL_GPIO_WritePin(AIN2_GPIO, AIN2_PIN, GPIO_PIN_SET);__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwmDuty);
}
上述函数中,我们通过 HAL_GPIO_WritePin
设置 AIN1/AIN2 的高低来控制方向,并通过 __HAL_TIM_SET_COMPARE
调节PWM占空比。其中 pwmDuty
参数可以是占空比对应的计数值。例如,如果TIM3->ARR(自动重装值,即Period)为 1000,我们传入500则约为50%占空比。使用这些函数时,要确保 TB6612 的 STBY 已经为高(芯片不在待机)。如果 STBY 引脚接到 STM32,需要在初始化时先 HAL_GPIO_WritePin(STBY_GPIO, STBY_PIN, GPIO_PIN_SET);
拉高。
停止电机有两种方式:
- 如上
Motor_Stop()
将方向引脚都置0实现快速刹车(短路制动)。此时电机轴会迅速停下。 - 如果想要电机自由滑行停止,可以调用如下函数而不是 Motor_Stop:
将 PWM 置0意味着不驱动电机,但如果 AIN1/AIN2 之前处于不同状态,则电机两端一个接地一个悬空,不会主动短路,所以电机惯性滑停。这种“coast”模式不会产生反向电流,但停止较慢。void Motor_Coast(void) {// 解除驱动使电机自由滑行: 保持AIN引脚当前状态或设为不同状态都可,主要是PWM=0__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);// 可选: 也可以将AIN1,AIN2同时设为HIGH实现另一种刹车模式,或保持原状 }
编码器脉冲计数与测量方法
我们采用定时器输入捕获来测量编码器脉冲间隔,从而计算速度。CubeMX 已配置 TIM2_CH1 为输入捕获并使能中断。CubeHAL 库在捕获事件发生时,会调用回调函数 HAL_TIM_IC_CaptureCallback()
. 我们需要实现这个回调函数来读取捕获值并计算脉冲周期。步骤如下:
-
定义变量: 需要变量保存上一次捕获的计数值,以及计算得到的周期和频率。由于TIM2计数可能溢出,我们也要处理溢出的情况。可以定义
lastCapture
、currCapture
、deltaCapture
等变量。还需要一个变量保存计算出的当前速度 (例如 RPM 或脉冲频率)。 -
实现捕获回调: 在
main.c
或相关文件中,实现 HAL 库的回调函数。例如:
/* 定时器输入捕获中断回调函数 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {if(htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {// 获取当前捕获值uint32_t capture_val = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);// 计算与上次捕获的差值(考虑溢出情况)if(capture_val >= lastCapture) {deltaCapture = capture_val - lastCapture;} else {// 计数器发生过溢出,需要加上最大值偏移deltaCapture = (htim->Init.Period - lastCapture + capture_val);}lastCapture = capture_val;// 根据捕获差值计算转速或频率// 假设定时器计数频率为 TimerClk Hz,则每个计数tick时间 = 1/TimerClk// 我们的deltaCapture即两个脉冲间的tick数。if(deltaCapture != 0) {float pulse_freq = (float)TimerClockHz / deltaCapture; // 每秒脉冲数currentSpeedRPM = (pulse_freq / pulsesPerRevolution) * 60.0f;} else {currentSpeedRPM = 0; // 如果deltaCapture意外为0(极小间隔),可视为极高速或错误,简单置0避免除零}}
}
在上述代码中:
HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1)
获取捕获寄存器的值(即当前脉冲对应的计数器刻度)。lastCapture
保存上一次的捕获刻度,需要在文件顶部定义为静态或全局变量,并初始化为0。deltaCapture
计算两个脉冲的计数差。TimerClockHz
是TIM2计数时钟频率(例如我们Prescaler设置1MHz计数,则 TimerClockHz = 1,000,000 Hz)。pulsesPerRevolution
是编码器每转一圈产生的脉冲数,需要根据你的电机编码器规格设置(例如编码器磁盘N对极则每转N脉冲,或者减速箱后脉冲数会乘齿轮比)。currentSpeedRPM
为全局变量,保存当前计算出的转速。
注意: 在实际使用前,需要先确定 TimerClockHz
和 pulsesPerRevolution
的值。比如:
const uint32_t TimerClockHz = 1000000; // 1 MHz
const uint32_t pulsesPerRevolution = 20; // 假设编码器每转20个脉冲
这些值可根据具体编码器参数调整。
如果使用外部中断计数法,实现会有所不同:
- 在
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
中捕获脉冲(每次脉冲调用),简单地pulseCount++
计数。 - 使用另一个定时器或 SysTick 定期(比如每100ms)读取
pulseCount
值,然后计算pulseCount * (1000ms/测量窗口ms) / pulsesPerRevolution * 60
作为RPM。读取后将pulseCount
清零继续累积。 - 外部中断要注意防抖,可以在硬件上加入小电容或在软件中判断过短时间的重复中断(霍尔传感器输出通常已是整形波,不需要特别防抖,但机械式的需要)。
本例采用捕获周期法,不需要周期定时器采样,但要考虑当转速很低或电机停转时,脉冲间隔会很长甚至没有脉冲:
- 可以设定一个上限时间,如果超过某个计数器值还未捕获新脉冲,则认为速度接近0。
- 或者在控制循环里监测如果一定时间没有更新速度,就将
currentSpeedRPM
设置为0。
计算电机转速 (RPM)
利用捕获得到的 deltaCapture
(脉冲间隔计数),我们已经在回调中计算了 currentSpeedRPM
。总结一下RPM计算公式:
RPM=TimerClockHzdeltaCapture×pulsesPerRevolution×60\text{RPM} = \frac{\text{TimerClockHz}}{\text{deltaCapture} \times \text{pulsesPerRevolution}} \times 60
其中 deltaCapture 除以 TimerClockHz 相当于脉冲周期 (秒),其倒数是脉冲频率 (每秒脉冲数),再除以编码器每转脉冲数得到每秒转数,乘60得到每分钟转数。
如果使用计数法,在每个固定时间窗口(例如0.1秒)测得脉冲数count:
RPM=countpulsesPerRevolution×60window_time_seconds.\text{RPM} = \frac{\text{count}}{\text{pulsesPerRevolution}} \times \frac{60}{\text{window\_time\_seconds}}.
例如100ms窗口,count= N脉冲,则RPM = (N/PPR) * (60 / 0.1) = (N/PPR) * 600。
在软件实现中,定时器捕获的计算通常更平滑连续,但需要处理好定时器溢出和长周期的情况;计数法简单直观,但低速时误差较大。可以结合两者:高速用捕获,低速或停转时检测count。
完成了电机驱动和传感部分的代码,我们就可以获取 currentSpeedRPM
等反馈,为实现闭环控制做准备。
5. PID 控制实现
有了实际速度/位置的反馈,我们就可以利用 PID 算法进行闭环控制。本节将介绍 PID 算法及其在代码中的实现,包括速度环和位置环的控制,以及 PID 参数的调校方法。
PID 控制算法介绍(位置式/增量式)
PID(Proportional-Integral-Derivative)控制器通过计算当前误差以及误差的累计和变化趋势来输出控制量。离散时间下,其基本公式(位置式)为:
u(k)=Kpe(k)+Ki∑i=0ke(i)+Kd[e(k)−e(k−1)]u(k) = K_p e(k) + K_i \sum_{i=0}^{k} e(i) + K_d [e(k) - e(k-1)]
其中 e(k)e(k) 是第 k 时刻的误差(设定值 - 测量值),u(k)u(k) 是控制输出(例如PWM占空比)。位置式PID直接计算输出的“绝对”值。
增量式PID关注控制增量,用差分形式表示输出变化:
Δu(k)=Kp[e(k)−e(k−1)]+Kie(k)+Kd[e(k)−2e(k−1)+e(k−2)]\Delta u(k) = K_p [e(k) - e(k-1)] + K_i e(k) + K_d [e(k) - 2e(k-1) + e(k-2)]
然后让 u(k)=u(k−1)+Δu(k)u(k) = u(k-1) + \Delta u(k)。这样计算时每次只用当前和前两次误差,节省计算且对累积误差不敏感。增量式PID的优点是不直接依赖累计和,因此在系统复位或计算溢出时不会造成输出突变;另外积分项在公式中隐含,通过每次误差叠加实现。
对于初学者,实现位置式PID直观且便于理解调试,因此我们以下用位置式PID公式讲解。核心步骤包括:
- 计算当前误差
error = setpoint - feedback
。 - 分别计算 P_term, I_term, D_term。
- 将三项相加得到输出,并考虑输出限幅。
- 更新历史误差和积分累积,为下一次计算做准备。
STM32 实现 PID 控制计算
我们可以使用 C 语言在 STM32 上实现 PID 控制。考虑实时性,一般在一个固定周期(如每10ms或每20ms)调用 PID 计算例程,这个周期称为控制周期或采样周期 dt。可以利用定时器中断或主循环中的 HAL_Delay 来实现固定周期。
首先,定义一个 PID 参数和状态的结构体便于管理:
typedef struct {float Kp;float Ki;float Kd;float target; // 目标值 (设定值)float integral; // 积分累积float prevError; // 前一次误差float output; // 上次输出 (位置式PID其实不一定需要存output,但可用于参考)float outputMax; // 输出上限float outputMin; // 输出下限
} PID_Controller;
初始化该结构体,比如针对速度控制的 PID:
PID_Controller pid_speed = {.Kp = 1.0f,.Ki = 0.5f,.Kd = 0.1f,.target = 0.0f,.integral = 0.0f,.prevError = 0.0f,.output = 0.0f,.outputMax = 1000.0f, // 假设PWM占空比最大值(ARR)为1000.outputMin = 0.0f // 占空比最小为0
};
然后实现 PID 计算函数(位置式算法):
float PID_Update(PID_Controller *pid, float feedback) {// 计算误差float error = pid->target - feedback;// 积分累加(考虑积分限幅,防止无限积累导致溢出)pid->integral += error;// 计算微分项float derivative = error - pid->prevError;// PID公式计算float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;// 输出限幅if(output > pid->outputMax) {output = pid->outputMax;// 抑制积分饱和:如果输出已达上限,避免积分项继续累积(抗饱和)pid->integral -= error; // 简单方式:回退本次积分}if(output < pid->outputMin) {output = pid->outputMin;pid->integral -= error;}// 保存状态pid->prevError = error;pid->output = output;return output;
}
该函数每次调用会根据当前反馈值计算新的输出。其中我们做了一点积分防饱和:当输出触及上下限时,减去本次积分增量以防止积分继续累积(这是一种简单的方法,还有更完善的积分分离、抗饱和方案,可在后续优化)。
控制周期:要保证 PID_Update 按固定时间间隔调用,否则参数调试会不稳定。比如我们选择 dt=0.01s (10ms),则 Ki 实际作用相当于公式中的 Ki*dt (因为积分每周期累加一次误差,相当于离散积分)。调参时也应考虑这个周期对行为的影响。
通过 PID 控制电机转速(速度环)
在速度闭环中,我们将目标转速 (RPM) 作为设定值,当前转速 (由编码器计算的 RPM) 作为反馈,使用 PID 输出调整PWM占空比。
结合前面的编码器读取,我们可以这样做:
- 定义并初始化速度 PID 控制器(如上 pid_speed)。
- 在主循环或定时中断中,周期性调用PID_Update计算新的PWM占空比。
- 将计算得到的 PWM输出值应用到电机驱动(即调整TIM3的CCR寄存器值)。
示例主循环伪代码,实现速度保持在设定值:
pid_speed.target = 100.0f; // 目标转速 100 RPM (示例)
uint32_t lastTime = HAL_GetTick();
uint32_t controlInterval = 10; // 控制周期 10mswhile(1) {if(HAL_GetTick() - lastTime >= controlInterval) {lastTime = HAL_GetTick();// 获取当前速度 (由编码器捕获中断更新的全局变量 currentSpeedRPM)float speedFeedback = currentSpeedRPM;// 计算PID输出 (新的PWM占空比值)float pwmOutput = PID_Update(&pid_speed, speedFeedback);// 应用到PWM输出,占空比为pwmOutput__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, (uint32_t)pwmOutput);}// 其他程序 ...
}
这里假定 TIM3->ARR = 1000,对应 pwmOutput 范围01000表示0100%占空比。如果 pid_speed.outputMax 设为1000,则 PID 输出直接是CCR的值。也可以把 PID 输出归一化为0~1再换算,但直接用计数值方便一些。
通过这种方式,PID 控制器会根据速度误差自动增减PWM。当电机转速低于目标时,误差为正,PID 输出增加 -> PWM提高,电机加速;当超过目标时,误差为负,PID输出降低 -> PWM减小,电机减速。经调试的 PID 可使 speedFeedback 接近 target,实现恒速。
通过 PID 控制电机位置(位置环)
位置控制相比速度控制要复杂一点。目标位置一般以编码器脉冲数或转过的圈数来表示。比如我们希望电机转轴旋转特定角度,可以折算成编码器脉冲目标。在位置环中,PID 控制器的误差是 “目标脉冲数 - 已经计数的脉冲数”,输出通常可以直接当作PWM占空比来驱动电机。然而,由于位置控制涉及方向,我们需要根据误差的正负来决定电机转动方向。
实现位置PID控制的一种简单方案:
- 使用单一 PID 控制器,但在计算输出后判断误差符号,将电机朝纠正误差的方向转动。
- 例如误差为正(还需要正方向前进),则设置电机Forward,误差为负(超出了目标需要反向),则设置Backward。输出的绝对值用作PWM占空比大小。
另一种更稳健的方法是串级控制:即外环位置PID产生速度指令,内环速度PID驱动PWM。这可以避免直接用位置误差产生PWM导致低速时震荡。但串级控制复杂度高,需要调整两个PID,本教程重点是入门实现,下面介绍直接位置PID的方法。
步骤:
- 设定目标位置(脉冲数)。需要一个全局变量
targetPosition
,和一个currentPosition
来累计编码器脉冲。currentPosition
可由编码器中断更新:每检测一个脉冲,按照当前电机方向增或减计数。如果只用单通道编码器,我们可以假设Motor_Forward时脉冲+=1,Motor_Backward时脉冲-=1。 - 初始化位置 PID 控制器参数 (Kp, Ki, Kd)。位置控制通常需要较小的Ki,因为积累误差太多可能导致振荡。Kd可以帮助减少冲过目标的情况。
- 定期计算位置误差 = targetPosition - currentPosition,然后让 PID 算出输出。这个输出我们可以视作“速度指令”或者“力度”(PWM值)。
- 根据误差正负决定方向:如果误差>0,表示还没达到目标,需要正转,误差<0需要反转。如果误差很小(在阈值内),认为到达,停止电机。
- 驱动电机:将 PWM 占空比设置为 PID输出大小,方向按上一步确定。
举例代码片段:
PID_Controller pid_position = {.Kp = 5.0f,.Ki = 0.0f,.Kd = 2.0f,.outputMax = 1000.0f,.outputMin = 0.0f
};
pid_position.target = 2000; // 目标位置,例如转到2000个脉冲处// 在主循环或定时器中断中:
float posFeedback = (float)currentPosition; // 当前已走脉冲数
float controlOut = PID_Update(&pid_position, posFeedback);
// PID输出controlOut,此时controlOut为需要的“驱动力”大小(0~1000)
if(pid_position.target - currentPosition > 0) {// 还需正转前进Motor_Forward((uint16_t)controlOut);
} else if(pid_position.target - currentPosition < 0) {// 超过目标,需要反转回去Motor_Backward((uint16_t)controlOut);
} else {// 误差为0,达到目标Motor_Stop();
}
在到位判定上,可以给一个误差死区,例如误差在 ±5 脉冲以内就认为到达目标,输出设为0避免来回抖动。也可以当误差很小且速度也很小的时候停止积分和电机驱动,锁定位置。
需要注意:直接用位置PID驱动有可能出现来回振荡的情况,因为电机有惯性,可能冲过目标再拉回。减小Kp、增加Kd有助于减轻此问题。如果要求精度高、响应快,可以考虑位置环套速度环:位置PID输出一个目标速度,速度PID再控制PWM。但对于初学学习,实现单环位置控制已经是不错的开始。
PID 参数调优方法
PID 参数的选择对控制效果影响很大。常用的调参方法包括理论计算法、试凑法和经验法。对于小车电机等系统,一般采用经验试凑逐步调整:
- 调整比例 Kp: 首先将 Ki, Kd 设为0,只调 Kp。增大 Kp 会加快响应速度、减小误差,但过大时系统会变得不稳定、产生振荡 (HAL库直流有刷电机的PWM驱动以及PID控制算法的实现_pid控制pwm来控制电机代码-CSDN博客)。逐渐提高 Kp 至电机的响应开始出现轻微振荡或快速逼近目标,然后取该值的约一半到三分之二作为最终 Kp。比如电机在Kp=10时开始明显振荡,就可以选 Kp≈6~7。
- 加入积分 Ki: 在获得一个可接受的Kp后,引入 Ki 以消除稳态误差。逐步增大 Ki,从很小的值开始。Ki 会推动误差积累后驱动输出,即使误差很小也能克服摩擦等实现零静差。但 Ki 太大会导致积累过多,引起超调和振荡。调整时可以让系统在某个设定值保持,看是否存在稳态误差(没有达到目标)。若有,则略微提高 Ki;若系统振荡变大,则减小 Ki。一般 Ki 调到系统在接受范围内稍有超调即可,不要追求过快消除误差,否则容易不稳定。
- 调整微分 Kd: Kd 项用于抑制变化,减缓冲击。逐渐增大 Kd,可以看到系统超调减小、振荡减弱。但 Kd 过大会让响应迟缓且对噪声(测量抖动)敏感,表现为输出抖动。对于电机这样相对响应快且测量可能有噪声的系统,Kd 值通常选较小。增大 Kd 直到刚好抑制大部分振荡即可。
- 反复微调: Kp, Ki, Kd 不是独立的,调整一个可能需要再回头调整另两个。在满足基本性能后,可以根据应用需求微调。例如需要更快响应则尝试略增 Kp 或 Ki,需要更稳则增 Kd 或降 Kp。
调参是一个不断尝试的过程。记录不同参数组合下系统的响应(比如给定阶跃目标时的上升时间、超调量、稳态误差、振荡周期)会有助于找出最佳值。在调试PID时,建议:
- 从速度环开始调,因为速度环动态快,参数容易看出效果。
- 位置控制可以在较低速度下测试,先用较小Kp保证不会剧烈来回,再逐步加快。
避免积分饱和: 如果发现系统长时间远离目标,积分项会累计很大,导致到达目标后出现长时间反向的纠错(overshoot很大)。这种情况下需要在误差变号时清除积分,或者在输出受限时冻结积分(在PID_Update实现中我们已做简易处理)。调整 Ki 也可以减轻该问题。
建议: 每次只改变一个参数,观察效果,再决定下步调整什么。最终的参数应该使得电机在负载变化时仍能稳定控制。如果负载范围变化大,可能需要折中参数或更高级的自适应控制,这超出本文范围。
6. 完整代码示例
结合以上各模块,这里给出一个简化的完整示例代码框架,包括 TB6612 驱动初始化、编码器测速、PID 控制速度和位置的实现,以及主循环逻辑。代码使用 STM32 HAL 库函数,读者需要根据自己使用的具体引脚名和定时器句柄调整部分代码。假设我们的设置如下:
- 使用 TIM3_CH1 (PA6) 输出 PWM 控制电机A速度。
- AIN1 -> PA1, AIN2 -> PA2 控制方向;STBY -> PA3 使能TB6612。
- 编码器A相 -> PA0 (TIM2_CH1输入捕获); B相如果有可接PB3(例如 TIM2_CH2)但此示例未用。
- 定时器TIM2输入捕获测频率计算速度; 位置通过累积脉冲计数(currentPosition)实现。
- 采用两个 PID 控制器:pid_speed 和 pid_position,可按需要启用其中一个环。
#include "main.h"
#include "tim.h"
#include "gpio.h"
#include <stdbool.h>
#include <stdint.h>// TB6612 引脚宏定义
#define AIN1_GPIO GPIOA
#define AIN1_PIN GPIO_PIN_1
#define AIN2_GPIO GPIOA
#define AIN2_PIN GPIO_PIN_2
#define STBY_GPIO GPIOA
#define STBY_PIN GPIO_PIN_3// 编码器参数
const uint32_t TimerClockHz = 1000000; // TIM2计数频率1MHz
const uint32_t pulsesPerRevolution = 20; // 编码器每转脉冲数 (举例)// 全局变量
volatile uint32_t currentPosition = 0; // 当前位置脉冲计数
volatile float currentSpeedRPM = 0.0f; // 当前转速(RPM)// PID控制结构体定义
typedef struct {float Kp;float Ki;float Kd;float target;float integral;float prevError;float output;float outputMax;float outputMin;
} PID_Controller;// PID 控制器实例
PID_Controller pid_speed = {0}, pid_position = {0};// PID初始化函数
void PID_Init(PID_Controller *pid, float Kp, float Ki, float Kd, float outputMin, float outputMax) {pid->Kp = Kp;pid->Ki = Ki;pid->Kd = Kd;pid->target = 0.0f;pid->integral = 0.0f;pid->prevError = 0.0f;pid->output = 0.0f;pid->outputMin = outputMin;pid->outputMax = outputMax;
}// PID更新计算
float PID_Update(PID_Controller *pid, float feedback) {float error = pid->target - feedback;pid->integral += error;float derivative = error - pid->prevError;float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;// 输出限幅及积分防饱和if(output > pid->outputMax) {output = pid->outputMax;pid->integral -= error; // 防积分饱和}if(output < pid->outputMin) {output = pid->outputMin;pid->integral -= error;}pid->prevError = error;pid->output = output;return output;
}// 电机驱动控制函数
void Motor_SetPWM(uint16_t pwm) {// 设置PWM占空比,不改变方向(用于速度控制时直接更新占空比)if(pwm > __HAL_TIM_GET_AUTORELOAD(&htim3)) {pwm = __HAL_TIM_GET_AUTORELOAD(&htim3); // 饱和保护}__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pwm);
}void Motor_Stop(void) {HAL_GPIO_WritePin(AIN1_GPIO, AIN1_PIN, GPIO_PIN_RESET);HAL_GPIO_WritePin(AIN2_GPIO, AIN2_PIN, GPIO_PIN_RESET);Motor_SetPWM(0);
}void Motor_Forward(void) {HAL_GPIO_WritePin(AIN1_GPIO, AIN1_PIN, GPIO_PIN_SET);HAL_GPIO_WritePin(AIN2_GPIO, AIN2_PIN, GPIO_PIN_RESET);
}void Motor_Backward(void) {HAL_GPIO_WritePin(AIN1_GPIO, AIN1_PIN, GPIO_PIN_RESET);HAL_GPIO_WritePin(AIN2_GPIO, AIN2_PIN, GPIO_PIN_SET);
}// 编码器中断/捕获回调处理
uint32_t lastCapture = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {if(htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {uint32_t capture_val = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);uint32_t deltaCapture;if(capture_val >= lastCapture) {deltaCapture = capture_val - lastCapture;} else {// 处理计数器溢出deltaCapture = (htim->Init.Period - lastCapture + capture_val);}lastCapture = capture_val;if(deltaCapture != 0) {float pulseFreq = (float)TimerClockHz / deltaCapture; // 当前脉冲频率currentSpeedRPM = (pulseFreq / pulsesPerRevolution) * 60.0f;} else {// deltaCapture==0理论上不可能(除非Timer频率极高电机极慢导致capture相等),保护一下currentSpeedRPM = 0;}// 更新位置计数// 单通道无法自动判方向,这里简单根据当前驱动方向更新:if(HAL_GPIO_ReadPin(AIN1_GPIO, AIN1_PIN) == GPIO_PIN_SET && HAL_GPIO_ReadPin(AIN2_GPIO, AIN2_PIN) == GPIO_PIN_RESET) {// 当前电机朝正转方向currentPosition++;} else if(HAL_GPIO_ReadPin(AIN1_GPIO, AIN1_PIN) == GPIO_PIN_RESET && HAL_GPIO_ReadPin(AIN2_GPIO, AIN2_PIN) == GPIO_PIN_SET) {// 当前电机朝反转方向currentPosition--;}}
}
上述代码实现了主要的控制函数和变量。在 main()
函数中,我们需要初始化这些模块并进入控制循环,例如:
int main(void) {// 初始化系统HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_TIM3_Init();MX_TIM2_Init();// 启动PWM输出和编码器捕获输入HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);// 如果使用TIM编码器模式则为: HAL_TIM_Encoder_Start(...) 略// 使能TB6612芯片HAL_GPIO_WritePin(STBY_GPIO, STBY_PIN, GPIO_PIN_SET);// 初始化 PID 控制器参数PID_Init(&pid_speed, 1.0f, 0.5f, 0.2f, 0.0f, __HAL_TIM_GET_AUTORELOAD(&htim3));PID_Init(&pid_position, 5.0f, 0.0f, 1.0f, 0.0f, __HAL_TIM_GET_AUTORELOAD(&htim3));// 示例: 控制模式选择bool speedControlMode = true;pid_speed.target = 120.0f; // 目标速度 120 RPMpid_position.target = 1000; // 目标位置 1000 个脉冲while(1) {// 控制循环 10msHAL_Delay(10);if(speedControlMode) {// 速度闭环控制// 设置电机朝目标方向转动,这里假设目标速度为正表示正转,负则反转if(pid_speed.target < 0) {Motor_Backward();} else {Motor_Forward();}// 计算新的PWM输出float pwmOut = PID_Update(&pid_speed, currentSpeedRPM);Motor_SetPWM((uint16_t)pwmOut);} else {// 位置闭环控制float posError = pid_position.target - currentPosition;float pwmOut = PID_Update(&pid_position, (float)currentPosition);if(fabs(posError) < 5.0f) {// 误差在5脉冲以内,认为到位,停止Motor_Stop();pwmOut = 0;} else if(posError > 0) {Motor_Forward();} else if(posError < 0) {Motor_Backward();}Motor_SetPWM((uint16_t)pwmOut);}}
}
代码说明:
- 我们假设一个
speedControlMode
变量决定是速度控制还是位置控制模式。在实际应用中可能根据需求选择,这里为了演示分别实现两种控制。 - 速度控制模式下,根据设定的
pid_speed.target
来决定方向(正/负)并计算 PWM。这里简单处理:如果目标速度为负则反转,正则前进。也可以有独立变量控制方向以避免用负值表示。 - 位置控制模式下,每循环计算当前位置误差
posError
。使用PID计算输出pwmOut
,然后根据误差正负设置方向。当误差很小(±5)时,调用Motor_Stop()
刹车并把 pwmOut 清零防止积分累积驱动。 - 控制周期用 HAL_Delay(10) 实现约10ms间隔(100Hz)。这简单易用,但定时精度受系统滴答影响。如果要求高,可用定时器中断或者自计时更精确。
- 注意在位置模式中,我们照样一直调用 PID_Update,即使停止后也在运行PID算法,这可能让积分项持续积累导致再次启动时出现问题。因此实际中当到位停止后,应该冻结 PID 或重置 integral。例如在判定到位时
pid_position.integral=0; pid_position.prevError=0;
并不再调用PID_Update直到新的目标给定。为简明未深入处理。
这个完整代码示例只是一个基本框架,用于演示如何串联起初始化、传感、控制和执行部分。读者应根据自己的硬件配置调整引脚和参数,并逐步调试。
7. 调试与优化
电机控制系统往往需要经过多次调试才能达到理想效果。下面提供一些调试和优化的建议,帮助定位问题、改进性能:
PWM 占空比调整与线性控制
- 校准占空比: 不同电机在不同占空比下的响应不一定线性。可以在开环模式下(不使用PID)手动给定几个占空比测试电机空载转速。例如20%、50%、80%占空比下的转速是多少RPM。这样可了解电机特性,辅助PID控制。例如某电机在20%以下可能无法启动克服静摩擦,这需要在PID输出很小时给予一个“起动力矩补偿”或干脆调整PID输出下限。
- 分辨率和频率: 确认PWM定时器的Period设置提供了足够的分辨率。Period值过低会导致调节不细,当PID输出变化小于一个计数时就无法反映。过高虽然提高分辨率但可能降低PWM频率或需要更高时钟。一般1000左右的Period(对应0.1%调节精度)已足够。PWM频率应高于电机机械响应频率很多倍,一般几kHz以上。也避免频率太高以致驱动芯片开关损耗大(TB6612能工作到很高频,但5-20kHz常用)。
- 死区控制: 对于双极性H桥(如TB6612),一般不需要专门设置死区时间。但要避免AIN1/AIN2同时高造成短路,通过逻辑避免即可。切换方向时先PWM设0再切换方向位,可以减少瞬间冲击。
编码器数据准确性分析
- 检查脉冲计数: 在低速转动电机一圈,手动数物理标记,看编码器计数是否等于标称值PPR,确保计数正确。如果差异大,可能是接线问题或者软件计数丢失。
- 捕获溢出: 在极低速时,两个脉冲间隔可能超过TIM2计数周期导致溢出。我们在捕获回调中处理了溢出但若溢出多次(电机停了很久)再来一个脉冲,我们简单计算可能低估间隔。这种情况下可以增加Period(降低计数频率)或者在没有脉冲来的期间手动设定速度0。当电机接近停转时,由于无法频繁捕获,可主动监测一定时间无脉冲则判定速度为0。
- 加装滤波: 编码器信号如果有抖动(可能由于机械振动导致霍尔传感器反复触发),可以在硬件上加入RC滤波,或在软件上设定一个最小脉冲间隔(如两次捕获若间隔过小则忽略)。STM32的定时器捕获也有数字滤波器,可在CubeMX中配置Input Filter(以内部时钟周期为单位)。例如设置滤波采样8个时钟周期等,可以抑制毛刺。
- 方向判断: 单通道编码器方案下,我们用控制命令的方向来决定计数增减,这在大部分情况可行。但若电机惯性超过目标导致反转回摆,或外力拖动电机逆向,会出现失算。为更可靠的方向和位置计数,应利用双通道编码器信号,STM32定时器Encoder模式可自动加减计数,这样无论什么原因转向变化都能捕获。但实现稍复杂,需要将两信号接到定时器CH1/CH2并配置 Encoder Interface,这超出本教程范围,可作为进阶学习方向。
PID 参数调整方法
- 分开调试: 建议先调试速度环PID,使电机能稳稳保持不同设定速度。记录在空载情况下,不同速度下系统稳定所需的参数。然后再调试位置环PID。位置环可在低速下测试,比如限制速度PID输出上限,以慢速移动到位置,避免高速冲击。
- 逐步增加复杂性: 开始可以只用 P 控制,看系统能否基本跟随。然后加 I 消除偏差,再加 D 改善动态。每加一项重新微调已有参数。
- 观察响应: 通过串口打印或者观察电机行为来判断响应情况。例如给一个阶跃目标,看速度曲线变化。如果有条件,可以用示波器/逻辑分析仪记录编码器脉冲频率变化,更直观了解调节过程。
- 积分限幅: 对于积分项,防止积分累计过多很重要。我们在代码中简单处理了积分在输出饱和时不增加。更好的方式是设定积分项的上下限值,如 integralMax,根据经验或系统需要设置。例如如果发现积分项累计超过某个阈值总是导致强烈振荡,可限制积分累积在该阈值内。
- 采样周期对PID的影响: 控制周期不同,PID参数效果不同。如果将控制周期改为5ms或者20ms,需要相应调整 Ki 和 Kd(因为离散积分和微分跟dt相关)。Kp相对独立(影响是误差的瞬时比例,和周期无关)。调参时确保周期固定且已知。
常见问题与解决方案
- 电机不转或只能一个方向转: 检查 TB6612 STBY 是否为高,AIN1/AIN2连接是否正确。用万用表或LED测试GPIO电平变化。如果只能一个方向,可能另一方向GPIO未正确配置或PWM没有输出到对应通道。
- 电机抖动/噪声大: 可能是 PWM 频率太低导致振动,或PID参数不稳定导致持续来回调节。尝试升高PWM频率或调低Kp/Ki。也可能是机械原因(松动或齿轮间隙),可忽略小误差避免反复纠正。
- 电机高速时控制失灵: 如果编码器脉冲过快,可能出现计数丢失或捕获不及时。可以降低速度要求,或检查STM32性能是否够用(一般F103完全能处理几十kHz的编码器)。也可能PID输出达到上限仍无法跟上目标,这是物理极限,应降低目标或增大电源电压/换电机。
- 启动冲击过大: 小负载电机在位置控制时可能一下冲过目标很多。这时可以在接近目标时减小允许的最大PWM输出(比如误差小于一定值时限制输出),或直接采用速度环控制过渡。简单方法是在位置PID外再套一层:当误差较大时,只给予中等PWM,不直接全功率冲刺。或者加大位置PID的Kd项让逼近时提前减速。
- 编码器计数方向反了: 如果用双通道发现计数相反,只要交换A相B相接入即可修正方向计数。单通道方案如果发现期望正转却currentPosition减少,那就在更新位置时把逻辑反过来即可。
通过一系列调试和优化,相信读者能够让 STM32F103C8T6 稳定地控制12V编码电机的速度和位置。本教程从基础原理出发,覆盖了硬件连接、软件配置和控制算法实现。希望通过完整的代码示例和详尽的解释,使初学者逐步建立闭环控制的概念并掌握实践方法。祝您在项目开发中取得成功!