文章目錄
- 硬件連接
- 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);