文章目录
- 硬件连接
- CubeMX配置
- **使用TIM5定时器CH3,即PA2作为脉冲控制,PE5控制方向(TIM5_CH4是为控制双电机做准备的可以先不配置)**
- 设置占空比为50%,以下为AI讲解
- 重要!!!定时器更新中断脉冲触发原理详解
- PWM脉冲生成
- 中断回调函数流程
- 详细工作流程
- 1.启动电机
- ***计算ARR值:***
- 初始化计数器和剩余步数:
- 2.PWM脉冲生成
- 3.中断处理过程
- **4.时间线示例**
- 1.为什么每个中断对应一个完整脉冲?
- 2.步数控制精度
- 3.停止机制
- 为什么这种方法精确?
- 梯形加减速代码
T型加减速
硬件连接
EN可以不接默认就是使能的
STP接PA2
DIR接PE5
COM接单片机3.3V
记得一定要共地
CubeMX配置
使用TIM5定时器CH3,即PA2作为脉冲控制,PE5控制方向(TIM5_CH4是为控制双电机做准备的可以先不配置)
设置占空比为50%,以下为AI讲解
定时器配置原理详解:PSC=0, ARR=83999
我将详细解释为什么在步进电机控制中设置PSC=0和ARR=83999这样的值,以及这些参数如何影响脉冲生成。让我们从基本原理开始:
定时器时钟系统基础
在STM32中,定时器的工作时钟由以下公式决定:
定时器时钟频率 = 系统时钟 / (PSC + 1)
对于STM32F4系列:
系统时钟通常为168 MHz
APB1总线定时器时钟为84 MHz (168 MHz / 2)
TIM5挂载在APB1总线上,所以其时钟为84 MHz
关键参数解析
PSC (预分频器 Prescaler)
PSC = 0
公式:实际分频系数 = PSC + 1
PSC=0 → 分频系数=1 → 定时器时钟频率=84 MHz
设置PSC=0的目的是保持最高计数频率,确保精确的时间控制
ARR (自动重装载值 Auto-Reload Register)
ARR = 83999
这个值决定了定时器的周期
当计数器从0计数到ARR时,产生一个更新事件
ARR值直接决定PWM频率
频率计算原理
PWM频率由以下公式决定:
PWM频率 = 定时器时钟频率 / (ARR + 1)代入实际值:PWM频率 = 84,000,000 Hz / (83999 + 1)=84,000,000 / 84,000=1000 Hz
所以ARR=83999对应的是1 kHz的PWM频率
为什么选择1 kHz?
在步进电机控制中,1 kHz是一个常见的起始频率:
安全范围:避免电机启动时失步
平滑启动:为加速曲线提供基础频率
通用性:适合大多数步进电机驱动器
易于计算:整数频率便于后续加速算法实现
实际应用中的计算
在您的步进电机控制函数中,实际使用的是动态计算的ARR值:
void SetMotorRPM(float rpm)
const uint32_t timer_clock = 84000000; // 84 MHzconst uint32_t steps_per_rev = 3200; // 200步/转 * 16微步// 计算所需频率 (Hz)float freq = (rpm * steps_per_rev) / 60.0f;// 计算ARR值uint32_t arr = (uint32_t)(timer_clock / freq) - 1;// 设置定时器__HAL_TIM_SET_AUTORELOAD(&htim5, arr);__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_3, arr / 2);
示例计算:60 RPM
计算频率:
转速 = 60 转/分钟
每转步数 = 3200 步
频率 = (60 × 3200) / 60 = 3200 Hz
计算ARR:
ARR = (84,000,000 / 3200) - 1 = 26,250 - 1 = 26,249
实际定时器行为:
计数器从0到26,249
当CNT < 13,124 (ARR/2) 时输出高电平
当CNT > 13,124 时输出低电平
当CNT=26,249时产生更新中断
为什么使用高ARR值?
在电机控制中,ARR值通常较大,原因如下:
精细控制:
-
更高的分辨率
更平滑的速度曲线 低速运行:
低RPM需要低频率
低频率需要高ARR值
10 RPM → ARR = (84e6)/(10*3200/60) - 1 = 157,499
避免整数舍入误差:
使用大ARR值减小量化误差
保持速度精度
即使在高RPM下,也能保持0.1125°的角度分辨率
脉冲周期误差小于0.005% (得益于84 MHz高时钟)
特殊值83999的深层含义
虽然您看到代码中有ARR=83999,但这只是初始化值或示例值。实际运行时ARR是动态计算的。83999的特殊性在于:
84,000,000 / (83,999 + 1) = 1,000
这代表了:
1 ms脉冲周期 (1 kHz)
方便的时间基准
整数关系便于心算验证
设置PSC=0和ARR=83999的本质是为了:获得最高的定时器计数频率(84 MHz),实现1 kHz的基础PWM频率,为动态速度控制提供精确的时间基准
在实际应用中,ARR值是根据RPM动态计算的,这确保了:
-
精确的脉冲频率控制
平滑的电机加减速
高分辨率的步进角度控制
优化的系统资源利用
代码motortest1.c
#include "motortest1.h"
#include "main.h"extern TIM_HandleTypeDef htim5;
//extern TIM_HandleTypeDef htim8;volatile uint32_t steps_remaining = 0; //剩余步数计数器,必须声明为volatile(因为会被中断修改)
volatile uint8_t motor_direction = 0; //
const uint32_t steps_per_revolution = 3200; // 200步/转 × 16细分
volatile uint32_t pulse_counter = 0; // 用于调试的脉冲计数器// 初始化函数
void Motor_Init(void)
{// 确保电机初始状态为停止HAL_TIM_PWM_Stop(&htim5, TIM_CHANNEL_3);steps_remaining = 0;motor_direction = 0;HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET);
}// 设置方向
void SetMotorDirection(uint8_t dir)
{if(dir)HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET); // 正转elseHAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET); // 反转 远离
}void StartMotor(void)
{__HAL_TIM_SET_COUNTER(&htim5, 0);// 重置计数器__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);// 清除中断标志HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_3);// 启动PWM输出HAL_TIM_Base_Start_IT(&htim5);// 启动更新中断
}void StopMotor(void)
{HAL_TIM_Base_Stop_IT(&htim5);// 停止中断HAL_TIM_PWM_Stop(&htim5, TIM_CHANNEL_3); // 停止PWM输出__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);// 清除中断标志
}// 设置电机转速(RPM)
// 参数: rpm - 期望转速(转/分钟)
void SetMotorRPM(float rpm)
{const uint32_t timer_clock = 84000000; // 84MHzconst uint32_t steps_per_rev = 3200; // 200步/转 * 16微步// 计算所需频率 (Hz)float freq = (rpm * steps_per_rev) / 60.0f;// 计算ARR值 (定时器重载值)uint32_t arr = (uint32_t)(timer_clock / freq) - 1;// 限制ARR范围if(arr > 65535) arr = 65535;if(arr < 100) arr = 100; // 最小值限制// 设置定时器周期和占空比__HAL_TIM_SET_AUTORELOAD(&htim5, arr);__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_3, arr / 2); // 50%占空比// 设置计数器为0__HAL_TIM_SET_COUNTER(&htim5, 0);// 清除中断标志__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);
}/*** @brief 以指定RPM移动指定步数* @param dir: 方向 (0=反转, 1=正转)* @param steps: 要移动的步数* @param rpm: 转速(转/分钟)* @retval None*/
void MoveStepsWithRPM(uint8_t dir, uint32_t steps, float rpm)
{// 停止任何正在进行的运动StopMotor();// 设置方向SetMotorDirection(dir);// 设置速度SetMotorRPM(rpm);// 更新剩余步数steps_remaining = steps;pulse_counter = 0; // 重置脉冲计数器// 启动运动if(steps > 0) {StartMotor();}
}// 定时器更新中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM5){// 每次更新中断对应一个完整的PWM周期// 即每个中断对应一个有效脉冲pulse_counter++;// 减少剩余步数if(steps_remaining > 0){steps_remaining--;}// 当步数为0时停止if(steps_remaining == 0){StopMotor();}}
}
motortest1.h
#ifndef MOTORTEST1_H
#define MOTORTEST1_H#include "stm32f4xx_hal.h"void Motor_Init(void);
void SetMotorDirection(uint8_t dir);
void SetMotorRPM(float rpm);
void StartMotor(void);
void StopMotor(void);
// 移动指定步数
void MoveStepsWithRPM(uint8_t dir, uint32_t steps, float rpm);#endif /* __STEPPER_MOTOR_H */
main.c
//步进电机中断函数初始化Motor_Init();HAL_TIM_Base_Start_IT(&htim5);MoveStepsWithRPM(0, 3200, 60);//0是远离
重要!!!定时器更新中断脉冲触发原理详解
在步进电机控制中,我们使用定时器的更新中断HAL_TIM_PeriodElapsedCallback来精确控制脉冲数量和电机步数。以下是详细的工作原理说明:
定时器工作原理
-
定时器是一个递增计数器(CNT),从0开始计数,当计数器达到自动重载值(ARR)时:
-
计数器重置为0
-
产生"更新事件"
-
触发更新中断(如果使能)
-
-
每个更新事件对应一个完整的PWM周期
PWM脉冲生成
__HAL_TIM_SET_AUTORELOAD(&htim5, arr); // 设置周期
__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_3, arr / 2); // 50%占空比
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM5){// 1. 脉冲计数pulse_counter++;// 2. 减少剩余步数if(steps_remaining > 0){steps_remaining--;}// 3. 检查是否完成if(steps_remaining == 0){StopMotor();}}
}
详细工作流程
1.启动电机
当调用MoveStepsWithRPM(0, 1600, 60)时:设置方向引脚(PE5),根据RPM(60转/分钟)计算PWM频率:
频率 = (RPM × 步数/转) / 60 =(60 × 3200) / 60=3200 Hz
计算ARR值:
ARR = (定时器时钟) / 频率 - 1 =84,000,000 / 3200 - 1 =26,249
初始化计数器和剩余步数:
steps_remaining = 1600pulse_counter = 0
2.PWM脉冲生成
定时器开始从0计数到26,249
当CNT < CCR(13,124)时,PA2输出高电平
当CNT > CCR时,PA2输出低电平
当CNT达到ARR(26,249)时:
CNT重置为0
产生更新事件
触发更新中断
3.中断处理过程
每次更新中断发生时:
1.脉冲计数:
pulse_counter++;
-
记录这是第几个脉冲
1600步对应1600次中断
2.步数递减:
if(steps_remaining > 0) {steps_remaining--;
}
- 每完成一个脉冲,减少一个剩余步数
3.完成检查:
if(steps_remaining == 0) {StopMotor();
}
-
当步数减到0时,停止电机
停止定时器中断和PWM输出
4.时间线示例
关键概念详解
1.为什么每个中断对应一个完整脉冲?
更新中断发生在CNT=ARR时
此时完成了一个完整的PWM周期:
-
从0开始上升到CCR(高电平)
从CCR继续到ARR(低电平)
然后重置到0,开始新周期
每个周期产生一个完整脉冲
2.步数控制精度
-
每个中断精确对应一个脉冲
1600次中断 = 1600个脉冲
没有累积误差
3.停止机制
当steps_remaining=0时:
void StopMotor()
// 1. 停止中断HAL_TIM_Base_Stop_IT(&htim5);// 2. 停止PWM输出
HAL_TIM_PWM_Stop(&htim5, TIM_CHANNEL_3);// 3. 清除中断标志
__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);
-
三重保护确保没有额外脉冲
立即停止,响应快速
为什么这种方法精确?
硬件级同步:脉冲计数与PWM生成完全同步,由定时器硬件保证精度
无累积误差:每个脉冲单独计数,不会因为浮点计算产生误差
确定性:中断在精确的时间点触发,不受软件延迟影响
实时响应:完成时立即停止,没有多余的脉冲
梯形加减速代码
这部分博主纯AI跑的,因为博主也不会,但只要和我配置的一样代码是可以运行的
motortest1.c
#include "motortest1.h"
#include "main.h"
#include "math.h"
#include <string.h>
#include <stdio.h>// 自定义数学常量 (避免依赖外部库)
#define M_PI 3.14159265358979323846f
#define M_PI_2 1.57079632679489661923f/* USER CODE END Includes */extern TIM_HandleTypeDef htim5;
//extern TIM_HandleTypeDef htim8;volatile uint32_t steps_remaining = 0; //剩余步数计数器,必须声明为volatile(因为会被中断修改)
volatile uint8_t motor_direction = 0; //
const uint32_t steps_per_revolution = 3200; // 200步/转 × 16细分
volatile uint32_t pulse_counter = 0; // 用于调试的脉冲计数器// 初始化函数
void Motor_Init(void)
{// 确保电机初始状态为停止HAL_TIM_PWM_Stop(&htim5, TIM_CHANNEL_3);steps_remaining = 0;motor_direction = 0;HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET);
}// 设置方向
void SetMotorDirection(uint8_t dir)
{if(dir)HAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_SET); // 正转elseHAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET); // 反转 远离
}void StartMotor(void)
{__HAL_TIM_SET_COUNTER(&htim5, 0);// 重置计数器__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);// 清除中断标志HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_3);// 启动PWM输出HAL_TIM_Base_Start_IT(&htim5);// 启动更新中断
}void StopMotor(void)
{HAL_TIM_Base_Stop_IT(&htim5);// 停止中断HAL_TIM_PWM_Stop(&htim5, TIM_CHANNEL_3); // 停止PWM输出__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);// 清除中断标志
}// 设置电机转速(RPM)
// 参数: rpm - 期望转速(转/分钟)
void SetMotorRPM(float rpm)
{const uint32_t timer_clock = 84000000; // 84MHzconst uint32_t steps_per_rev = 3200; // 200步/转 * 16微步// 计算所需频率 (Hz)float freq = (rpm * steps_per_rev) / 60.0f;// 计算ARR值 (定时器重载值)uint32_t arr = (uint32_t)(timer_clock / freq) - 1;// 限制ARR范围if(arr > 65535) arr = 65535;if(arr < 100) arr = 100; // 最小值限制// 设置定时器周期和占空比__HAL_TIM_SET_AUTORELOAD(&htim5, arr);__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_3, arr / 2); // 50%占空比// 设置计数器为0__HAL_TIM_SET_COUNTER(&htim5, 0);// 清除中断标志__HAL_TIM_CLEAR_FLAG(&htim5, TIM_FLAG_UPDATE);
}/*** @brief 以指定RPM移动指定步数* @param dir: 方向 (0=反转, 1=正转)* @param steps: 要移动的步数* @param rpm: 转速(转/分钟)* @retval None*/
void MoveStepsWithRPM(uint8_t dir, uint32_t steps, float rpm)
{// 停止任何正在进行的运动StopMotor();// 设置方向SetMotorDirection(dir);// 设置速度SetMotorRPM(rpm);// 更新剩余步数steps_remaining = steps;pulse_counter = 0; // 重置脉冲计数器// 启动运动if(steps > 0) {StartMotor();}
}// 在motortest1.c中实现TrapezoidalProfile speed_profile;void InitTrapezoidalProfile(uint32_t steps, float max_rpm, float accel, float decel)
{// 计算速度转换因子 (RPM -> 步数/秒)const float rpm_to_steps_per_sec = steps_per_revolution / 60.0f;// 计算最大速度 (步数/秒)float max_speed = max_rpm * rpm_to_steps_per_sec;// 计算加速度 (步数/秒2)float accel_steps = accel * rpm_to_steps_per_sec;float decel_steps = decel * rpm_to_steps_per_sec;// 计算加速所需步数speed_profile.accel_steps = (uint32_t)((max_speed * max_speed) / (2.0f * accel_steps));// 计算减速所需步数speed_profile.decel_steps = (uint32_t)((max_speed * max_speed) / (2.0f * decel_steps));// 计算匀速阶段步数if (speed_profile.accel_steps + speed_profile.decel_steps < steps) {speed_profile.const_steps = steps - speed_profile.accel_steps - speed_profile.decel_steps;} else {// 如果步数不足以达到最大速度,调整加速和减速步数speed_profile.accel_steps = steps / 2;speed_profile.decel_steps = steps / 2;speed_profile.const_steps = 0;}// 设置其他参数speed_profile.total_steps = steps;speed_profile.max_rpm = max_rpm;speed_profile.accel = accel;speed_profile.decel = decel;speed_profile.step_count = 0;speed_profile.current_rpm = 0.0f; // 从0开始加速
}void ApplySpeedProfile(void)
{if (speed_profile.step_count < speed_profile.accel_steps) {// 加速阶段float factor = (float)speed_profile.step_count / (float)speed_profile.accel_steps;speed_profile.current_rpm = speed_profile.max_rpm * factor;} else if (speed_profile.step_count < (speed_profile.accel_steps + speed_profile.const_steps)) {// 匀速阶段speed_profile.current_rpm = speed_profile.max_rpm;} else {// 减速阶段uint32_t decel_start = speed_profile.accel_steps + speed_profile.const_steps;uint32_t steps_into_decel = speed_profile.step_count - decel_start;float factor = 1.0f - ((float)steps_into_decel / (float)speed_profile.decel_steps);speed_profile.current_rpm = speed_profile.max_rpm * factor;}// 应用当前速度到电机SetMotorRPM(speed_profile.current_rpm);// 增加步数计数speed_profile.step_count++;
}void MoveStepsWithProfile(uint8_t dir, uint32_t steps, float max_rpm, float accel, float decel)
{// 停止任何正在进行的运动StopMotor();// 设置方向SetMotorDirection(dir);// 初始化速度曲线InitTrapezoidalProfile(steps, max_rpm, accel, decel);// 启动运动StartMotor();
}// 修改定时器更新中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM5){// 应用速度曲线ApplySpeedProfile();// 减少剩余步数if (speed_profile.total_steps > 0) {speed_profile.total_steps--;}// 当步数为0时停止if (speed_profile.total_steps == 0) {StopMotor();}}
}
motortest1.h
#ifndef MOTORTEST1_H
#define MOTORTEST1_H#include "stm32f4xx_hal.h"void Motor_Init(void);
void SetMotorDirection(uint8_t dir);
void SetMotorRPM(float rpm);
void StartMotor(void);
void StopMotor(void);
// 移动指定步数
void MoveStepsWithRPM(uint8_t dir, uint32_t steps, float rpm);// 在motortest1.h中添加
typedef struct {float max_rpm; // 最大转速 (RPM)float accel; // 加速度 (RPM/s)float decel; // 减速度 (RPM/s)uint32_t total_steps; // 总步数uint32_t accel_steps; // 加速阶段步数uint32_t decel_steps; // 减速阶段步数uint32_t const_steps; // 匀速阶段步数float current_rpm; // 当前转速uint32_t step_count; // 当前步数计数
} TrapezoidalProfile;void InitTrapezoidalProfile(uint32_t steps, float max_rpm, float accel, float decel);
void ApplySpeedProfile(void);
void MoveStepsWithProfile(uint8_t dir, uint32_t steps, float max_rpm, float accel, float decel);#endif /* __STEPPER_MOTOR_H */
main.c
//步进电机中断函数初始化Motor_Init();HAL_TIM_Base_Start_IT(&htim5);// 移动1600步,最大速度300 RPM,加速度30 RPM/s,减速度30 RPM/sMoveStepsWithProfile(0, 3600, 300, 30, 30);