本文将详细介绍如何使用 STM32F103C8T6 微控制器,通过 TB6612 双电机驱动芯片驱动 12V 直流编码电机。我们将采用标准 PWM 调速方式,并利用 PID 控制算法实现电机转速和位置的闭环控制。教程内容从基础原理开始,逐步涵盖硬件连接、开发环境配置、驱动代码实现、PID 控制算法以及完整实例代码,最后提供调试与优化的建议。即使是零基础的读者,通过本教程也能逐步掌握相关知识和实现方法。
在开始实际操作之前,需要了解一些基础理论,包括电机驱动芯片 TB6612 的工作原理、编码电机(带霍尔传感器)的工作方式、PWM 调速原理,以及 PID 控制在闭环控制中的作用。
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)。其中重要的控制引脚有:
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 引脚控制电机速度,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两路信号),通过这两路信号可以判定转动方向并更精准地计数脉冲。对于初学者,如果电机只提供一路霍尔信号,我们仍可通过记录脉冲数量来得到速度,并利用控制信号方向来推断电机的转动方向。
通过霍尔传感器测速的基本方法有两种:
编码器不仅可用于测速,也可用于测位置。通过累计脉冲数,我们可以知道电机轴旋转了多少圈或多少角度。例如,如果编码器每圈输出P个脉冲,那么累计计数达到P就代表轴转了一整圈。结合转向信息(由A/B两路或由控制指令知道),可以增加或减少计数,实现对相对位置的检测。如果需要绝对位置,通常要有参考零点或者使用更复杂的绝对编码器。本教程的电机位置控制基于相对脉冲计数,即通过设定脉冲目标值来控制电机转到目标位置。
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,我们可以线性地控制电机速度 (Brief introduction of tb6612fng and its design of DC motor control system with single chip microcomputer - Latest Open Tech From Seeed)。频率一般选择较高值(几千Hz以上)以避免电机产生可闻噪音和降低电流脉动。STM32 定时器可以方便地产生PWM波形,微控制器只需调整占空比(比较值),就能实时改变电机速度。
开环控制(不使用反馈)时,我们仅设置PWM占空比让电机转动,无法确保实际达到期望的速度或位置。例如,负载变化或电压波动会导致电机转速偏离设定值。而闭环控制(反馈控制)通过传感器获取实际速度/位置,并根据期望与实际的误差不断调整驱动输入,从而使电机输出跟随设定目标。
PID 控制器是一种常用且高效的闭环控制算法,由比例 (P)、积分 (I)、微分 (D) 三部分组成:
在电机速度控制中,PID 控制器根据“期望转速”和“实际转速”计算误差,不断调整PWM占空比,使实际转速逼近期望值,实现恒速控制。在位置控制中,PID 控制器根据“目标位置”(目标脉冲数)和“当前累计脉冲数”计算误差,调整电机转动方向和速度,使电机移动到目标位置并稳定在那里。
PID 控制属于闭环控制的核心部分,它的引入使得系统能自动补偿外界干扰和参数变化。例如,当上坡导致电机变慢时,速度误差增大,PID 控制会自动提高PWM占空比增大扭矩;在位置控制中,当接近目标位置时误差减小,PID 会降低驱动力度防止冲过头。合理调整 PID 三个参数 (Kp、Ki、Kd) 可以使电机实现快速响应、稳准跟踪、无静差的控制效果。
延伸: PID 控制算法有两种常见实现形式——位置式和增量式。位置式PID直接计算控制量的绝对值,而增量式PID计算控制量的增量,优点是对计算误差累积不敏感。后文将对这两种实现有所提及。
了解原理后,我们需要将 STM32 微控制器、TB6612 驱动板、编码电机和电源正确连接。本节介绍电机驱动和编码器信号的接线方法,以及12V供电的注意事项。
首先确定使用 TB6612 的哪个通道来驱动电机。如果只驱动一台电机,我们可以使用 TB6612 的通道A(对应 AIN1, AIN2, PWMA 等引脚)。连接方法如下:
电源连接:
电机连接:
控制信号连接:
连接完成后,STM32 就可以通过PWM引脚输出的占空比控制转速,通过AIN1/AIN2的电平组合控制转动方向或刹车。务必检查共地,以及所有控制引脚电压均不超过TB6612 VCC电压。
编码电机上通常引出编码器的信号线。以常见的双霍尔(两相 AB 相位)编码器为例,会有A相、B相两根信号线(还有电源供电线,一般为 Vcc 和 GND)。若是单霍尔传感器,则只有一根脉冲输出线和电源线。典型的霍尔传感器工作电压为5V或3.3V,如果编码器模块标称5V且输出也是5V电平,直接接STM32(3.3V逻辑)需要注意电平兼容,可通过电阻分压或逻辑电平转换。很多情况下霍尔传感器输出为开漏集电极,需要上拉电阻,可利用STM32的内部上拉或外接上拉到3.3V来获得可靠的3.3V脉冲信号。
将编码器信号连接 STM32 的方法:
如果使用定时器捕获模式读取速度:选用STM32的一个定时器通道作为输入捕获。在硬件上,将该定时器通道对应的引脚连接到编码器脉冲输出。例如 TIM2_CH1 (PA0) 或 TIM3_CH1 (PA6) 等。该引脚在 CubeMX 中配置为 “GPIO_Input” 并开启 “TIMx CHy Input Capture” 功能。硬件连接完成后,我们可以在软件中捕获脉冲的时间间隔或频率,进而算出转速。
由于电机使用12V供电,而且电机启动瞬间和堵转时电流较大(可能几百毫安到数安培,视电机而定),电源部分需要注意以下几点:
完成以上硬件连接与电源准备后,整个系统的结构如下:STM32通过PWM和方向引脚控制TB6612,TB6612驱动电机;编码器霍尔传感器将电机转动反馈给STM32输入引脚;12V电源为电机提供动力并通过降压为控制电路供电。确认连接无误后即可进行软件配置和编程。
在开始编写代码前,我们需要搭建 STM32 的开发环境并进行初步配置。我们将使用 STM32CubeIDE(集成了 CubeMX 图形配置和IDE)来创建工程,通过 CubeMX 配置 PWM 和定时器输入捕获(或外部中断)等外设,从而生成基础初始化代码。
接下来配置定时器PWM输出,以产生控制 TB6612 所需的 PWM 信号:
PWM_frequency = TimerClk / ((Prescaler+1)*(Period+1))
。例如,Prescaler=71,Period=99,将得到约10kHz的PWM(72MHz/(72*100) = 10kHz)。注意: 实际计算需根据 Timer 时钟计算。CubeMX 也提供直接填写期望频率的选项(在 PWM 设置中可填写 Desired frequency,然后调整Period/Prescaler)。配置完成后,TIM3_CH1 将输出 PWM 波形。稍后我们会在代码中调用 HAL 库函数启动 PWM 输出,并根据 PID 计算结果修改占空比。
为了获取编码器的反馈信号,我们需要配置 STM32 的输入捕获或外部中断。这里介绍使用定时器输入捕获测量编码器脉冲周期的方法:
如果采用外部中断计数方法(不测周期而是在固定时间内计数):可以在 CubeMX 中不使用定时器捕获,而改为:
本教程以定时器输入捕获方案为例,因为它可以直接测量脉冲间隔,方便计算即时速度。但初学者如觉得复杂,也可以用 EXTΙ 方法,通过周期定时读取累计脉冲数计算平均速度。
当工程初始化代码生成后,我们需要编写控制电机和读取编码器的代码。本节将介绍 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=0
HAL_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=1
HAL_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实现快速刹车(短路制动)。此时电机轴会迅速停下。void Motor_Coast(void) {
// 解除驱动使电机自由滑行: 保持AIN引脚当前状态或设为不同状态都可,主要是PWM=0
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
// 可选: 也可以将AIN1,AIN2同时设为HIGH实现另一种刹车模式,或保持原状
}
将 PWM 置0意味着不驱动电机,但如果 AIN1/AIN2 之前处于不同状态,则电机两端一个接地一个悬空,不会主动短路,所以电机惯性滑停。这种“coast”模式不会产生反向电流,但停止较慢。我们采用定时器输入捕获来测量编码器脉冲间隔,从而计算速度。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++
计数。pulseCount
值,然后计算 pulseCount * (1000ms/测量窗口ms) / pulsesPerRevolution * 60
作为RPM。读取后将 pulseCount
清零继续累积。本例采用捕获周期法,不需要周期定时器采样,但要考虑当转速很低或电机停转时,脉冲间隔会很长甚至没有脉冲:
currentSpeedRPM
设置为0。利用捕获得到的 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
等反馈,为实现闭环控制做准备。
有了实际速度/位置的反馈,我们就可以利用 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
。我们可以使用 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 (因为积分每周期累加一次误差,相当于离散积分)。调参时也应考虑这个周期对行为的影响。
在速度闭环中,我们将目标转速 (RPM) 作为设定值,当前转速 (由编码器计算的 RPM) 作为反馈,使用 PID 输出调整PWM占空比。
结合前面的编码器读取,我们可以这样做:
示例主循环伪代码,实现速度保持在设定值:
pid_speed.target = 100.0f; // 目标转速 100 RPM (示例)
uint32_t lastTime = HAL_GetTick();
uint32_t controlInterval = 10; // 控制周期 10ms
while(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 控制器的误差是 “目标脉冲数 - 已经计数的脉冲数”,输出通常可以直接当作PWM占空比来驱动电机。然而,由于位置控制涉及方向,我们需要根据误差的正负来决定电机转动方向。
实现位置PID控制的一种简单方案:
另一种更稳健的方法是串级控制:即外环位置PID产生速度指令,内环速度PID驱动PWM。这可以避免直接用位置误差产生PWM导致低速时震荡。但串级控制复杂度高,需要调整两个PID,本教程重点是入门实现,下面介绍直接位置PID的方法。
步骤:
targetPosition
,和一个 currentPosition
来累计编码器脉冲。currentPosition
可由编码器中断更新:每检测一个脉冲,按照当前电机方向增或减计数。如果只用单通道编码器,我们可以假设Motor_Forward时脉冲+=1,Motor_Backward时脉冲-=1。举例代码片段:
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时,建议:
避免积分饱和: 如果发现系统长时间远离目标,积分项会累计很大,导致到达目标后出现长时间反向的纠错(overshoot很大)。这种情况下需要在误差变号时清除积分,或者在输出受限时冻结积分(在PID_Update实现中我们已做简易处理)。调整 Ki 也可以减轻该问题。
建议: 每次只改变一个参数,观察效果,再决定下步调整什么。最终的参数应该使得电机在负载变化时仍能稳定控制。如果负载范围变化大,可能需要折中参数或更高级的自适应控制,这超出本文范围。
结合以上各模块,这里给出一个简化的完整示例代码框架,包括 TB6612 驱动初始化、编码器测速、PID 控制速度和位置的实现,以及主循环逻辑。代码使用 STM32 HAL 库函数,读者需要根据自己使用的具体引脚名和定时器句柄调整部分代码。假设我们的设置如下:
#include "main.h"
#include "tim.h"
#include "gpio.h"
#include
#include
// 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 RPM
pid_position.target = 1000; // 目标位置 1000 个脉冲
while(1) {
// 控制循环 10ms
HAL_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 清零防止积分累积驱动。pid_position.integral=0; pid_position.prevError=0;
并不再调用PID_Update直到新的目标给定。为简明未深入处理。这个完整代码示例只是一个基本框架,用于演示如何串联起初始化、传感、控制和执行部分。读者应根据自己的硬件配置调整引脚和参数,并逐步调试。
电机控制系统往往需要经过多次调试才能达到理想效果。下面提供一些调试和优化的建议,帮助定位问题、改进性能:
通过一系列调试和优化,相信读者能够让 STM32F103C8T6 稳定地控制12V编码电机的速度和位置。本教程从基础原理出发,覆盖了硬件连接、软件配置和控制算法实现。希望通过完整的代码示例和详尽的解释,使初学者逐步建立闭环控制的概念并掌握实践方法。祝您在项目开发中取得成功!