记录一次使用面向对象的C语言封装步进电机驱动
简介
(2025/4/21)
本库对目前仅针对TB6600驱动下的42步进电机的基础功能进行了一定的封装, 也是我初次尝试以面向对象的思想去编写嵌入式代码, 和直流电机的驱动步骤相似在调用stepmotor_attach()函数和stepmotor_init()函数之后仅通过结构体数组stepm然后指定枚举变量中的id即可完成对步进电机的基础操作, 其中最核心的是控制函数step_move的实现, 该函数可以在开环状态下指定步进电机的步数和频率(速度)进行控制, 后续可能会更新一些经典的控制模型如梯形加减速.
先贴一下项目代码
项目源码
cubemx 配置
如果要使用此库, 你只需要在cubemx中完成以下配置:
- 打开定时器的PWM通道并将 Prescaler设置为83(我的时钟主频为84分频后为1MHz, 这个并不是一定得是1MHz, 后面会说)
- 打开NVIC
- 配置相关的GPIO, 如方向引脚, 这个很简单我就不贴图了
移植
如果你的硬件平台和我一样, 无脑粘贴就行, 如果不一样, 则需要修改相关代码
- 修改类型(如果不是stm32HAL库)
主要是硬件层结构体的类型声明, 和stepmotor_attach函数, 需要修改类型
typedef struct {// todo 硬件参数层TIM_HandleTypeDef* htim; //定时器句柄uint32_t Channel; // 输出通道GPIO_TypeDef* enType; //使能引脚类别uint16_t enPin; //使能引脚pinGPIO_TypeDef* dirType; //方向引脚类别uint16_t dirPin; //方向引脚pin
}STEPMOTOR_HARDWARE;
void stepmotor_attach(STEPID id,TIM_HandleTypeDef* htim,uint32_t Channel, // 输出通道GPIO_TypeDef* enType, //使能引脚类别uint16_t enPin, //使能引脚pinGPIO_TypeDef* dirType, //方向引脚类别uint16_t dirPin //方向引脚pin);
2. 底层接口函数封装
这里都添加在了step_motor.c中的部分以static声明的函数当中, 根据注释功能修改函数的实现:
static void en_set(STEPID id){// todo 使能置高STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];hw->enType->BSRR = hw->enPin;
}
static void en_reset(STEPID id){// todo 使能置低STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];hw->enType->BSRR = (uint32_t)hw->enPin << 16U; // 置位
}static void dir_set(STEPID id){// todo 方向置高STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];hw->dirType->BSRR = hw->dirPin;
}
static void dir_reset(STEPID id){// todo 方向置低STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];hw->dirType->BSRR = (uint32_t)hw->dirPin << 16U; // 置位
}static void tim_start(STEPID id){// todo 定时器启动STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];hw->htim->Instance->CR1 |= TIM_CR1_CEN;
}
static void tim_stop(STEPID id){// todo 定时器停止并清0STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];__HAL_TIM_SET_COUNTER(hw->htim, 0);__HAL_TIM_CLEAR_FLAG(hw->htim, TIM_FLAG_UPDATE);
}static void pwm_start(STEPID id){// todo pwm启动STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];HAL_TIM_PWM_Start(hw->htim, hw->Channel);
}
static void pwm_stop(STEPID id){// todo pwm停止 其实将比较值赋为0就行STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];HAL_TIM_PWM_Stop(hw->htim, hw->Channel);
}static void pwm_startIT(STEPID id){// todo pwm中断开启STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];HAL_TIM_PWM_Start_IT(hw->htim, hw->Channel);
}
static void pwm_stopIT(STEPID id){// todo pwm中断停止STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];HAL_TIM_PWM_Stop_IT(hw->htim, hw->Channel);
}
static void pwm_setcompare(STEPID id, int32_t freq){// todo 设置50%的占空比// todo 重新设置占空比(占空比永远为比较值的一半, 即50%占空比)STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];if(freq > 0){hw->htim->Instance->ARR = Hclk/freq - 1;__HAL_TIM_SET_COMPARE(hw->htim, hw->Channel, hw->htim->Instance->ARR/2); // 占空比无所谓 一半即可}
}
3. 频率宏定义
在step_motor.h中的:
#define Hclk 1000000 //时间总频
这个是预分频后的频率
根据实际情况来, 如果你是72MHz主频, 定时器的Prescaler设置为71, 则不用改, 因为分频后依然为1MHz
如果你的定时器的Prescaler值设置为0, 那么这个就是你的时钟主频了
这个值参与ARR的赋值计算(在**static void pwm_setcompare(STEPID id, int32_t freq);**中), 所以你得好好根据自己ARR的量程来, 像c8t6是65535最大, 那么你最好将Prescaler值给大一些
4. 中断修改(如果不是stm32HAL库)
具体的修改逻辑看看下面的步数&速度控制这个标题下的内容, 根据你的平台中断逻辑进行修改.
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{if(htim == stepm[SM1].hw->htim) // 回调函数检测为某个步进电机对应的定时器{Stepper_UpdateHandler(SM1);}
}
初始化
成员变量概览
先看看一个对象所包含的属性, 这里我都敲了注释, 主要是使用STEPMOTOR作为对象的类型.
typedef struct {// todo 硬件参数层TIM_HandleTypeDef* htim; //定时器句柄uint32_t Channel; // 输出通道GPIO_TypeDef* enType; //使能引脚类别uint16_t enPin; //使能引脚pinGPIO_TypeDef* dirType; //方向引脚类别uint16_t dirPin; //方向引脚pin// todo 硬件API重写层STEPMOTOR_INTERFACE enSet; // 方向引脚置位STEPMOTOR_INTERFACE enReset; // 方向引脚置位STEPMOTOR_INTERFACE dirSet; // 方向引脚置位STEPMOTOR_INTERFACE dirReset; // 方向引脚置位STEPMOTOR_INTERFACE htimStop; // 停止并复位定时器STEPMOTOR_INTERFACE htimStart; // 重新启动定时器STEPMOTOR_INTERFACE pwmStop; // pwm停止STEPMOTOR_INTERFACE pwmStart; // pwm启动STEPMOTOR_INTERFACE pwmStopIT; // pwm中断计数停止STEPMOTOR_INTERFACE pwmStartIT; // pwm中断计数启动STEPMOTOR_SPEED_INTERFACE pwmSetCompare; // pwm设置为50%的占空比}STEPMOTOR_HARDWARE;typedef struct{STEPMOTOR_HARDWARE* hw; // 硬件接口封装层// todo 参数层uint32_t cur_freq; // 当前频率Hzuint16_t cur_step; // 当前步数uint8_t dir; // 当前方向// todo 目标值uint16_t tar_step; // 目标步数// todo 限制层uint8_t is_limit_step; // 你是否要限制步数uint8_t is_finish; // 是否完成路程int16_t accumulate_step; // 累计步数int32_t max_step; // 最大步数 - 配合累计步数以限幅int32_t min_step; // 最小步数 - 配合累计步数以限幅uint32_t min_freq; // 最小运行频率uint32_t max_freq; // 最大运行频率// todo 函数接口层STEPMOTOR_INTERFACE limitStep;STEPMOTOR_INTERFACE noLimitStep;STEPMOTOR_STEP_INTERFACE stepMove; // 指定步数和速度进行移动STEPMOTOR_INTERFACE stop; // 立即停止STEPMOTOR_RANGE_INTERFACE setRange; // 设置相关范围的接口}STEPMOTOR;
我们需要将step_motor.c和step_motor.h添加到你的工程目录下面, 然后调用初始化函数
void stepmotor_attach(STEPID id, // 电机idTIM_HandleTypeDef* htim, // 电机对应定时器uint32_t Channel, // 输出通道GPIO_TypeDef* enType, //使能引脚类别uint16_t enPin, //使能引脚pinGPIO_TypeDef* dirType, //方向引脚类别uint16_t dirPin //方向引脚pin);
stepmotor_init();
前者stepmotor_attach是引脚定向, 为了后面我们可以用结构体数组引出各种属性和函数进行调用, 以下是函数的实现流程
void stepmotor_attach(STEPID id,TIM_HandleTypeDef* htim,uint32_t Channel, // 输出通道GPIO_TypeDef* enType, //使能引脚类别uint16_t enPin, //使能引脚pinGPIO_TypeDef* dirType, //方向引脚类别uint16_t dirPin //方向引脚pin){STEPMOTOR_HARDWARE* hw = &stepmotorhws[id];// todo 硬件引脚重指定hw->htim = htim; // 硬件接口初始化hw->Channel = Channel;hw->enType = enType;hw->enPin = enPin;hw->dirType = dirType;hw->dirPin = dirPin;// todo 硬件函数接口hw->enSet = en_set;hw->enReset = en_reset;hw->dirSet = dir_set;hw->dirReset = dir_reset;hw->htimStart = tim_start;hw->htimStop = tim_stop;hw->pwmStop = pwm_stop;hw->pwmStart = pwm_start;hw->pwmStartIT = pwm_startIT;hw->pwmStopIT = pwm_stopIT;hw->pwmSetCompare = pwm_setcompare;// todo 控制接口函数stepm[id].hw = hw;stepm[id].limitStep = limit_step;stepm[id].noLimitStep = no_limit_step;stepm[id].stepMove = step_move; // 移动函数接口赋值stepm[id].stop = step_stop; // 立即停止接口指定stepm[id].setRange = set_range; // 范围设置指向
}
stepmotor_init() 是初始化函数, 在这里cubemx已经初始化完成, 我只添加了相关外设如定时器中断等启动函数也可以添加自己的初始化代码.
void stepmotor_init(void){for(uint8_t i = 0; i < STEP_SUM; i++){STEPMOTOR* m = &stepm[i]; // 获取对象指针// todo 其他初始化// todo 启动m->hw->pwmStartIT(i); // PWM中断m->hw->enSet(i); // 使能引脚使能}
}
使用库进行控制
步数&速度控制
目前没有添加太多的算法, 仅仅是开环的指定速度和位移移动的函数, 不过这应该也是后续底层最为核心的函数:
// 指定id id 指定走多少步 指定速度/频率
stepm[STEPID].stepMove(STEPID, int32_t, uint32_t);
仅仅是使用的话, 只需要传入对应电机对象id和相关参数即可, 其中第二个参数是用于指定目标是多少步, 支持正负号, 可以输入负值, 它意味着电机朝反方向转.
同时, 因为我们在初始化配置的时候开启了PWM中断所以在这里我们也需要在中断中添加部分代码:
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{if(htim == stepm[SM1].hw->htim) // 回调函数检测为某个步进电机对应的定时器{Stepper_UpdateHandler(SM1);}
}
在中断回调函数HAL_TIM_PWM_PulseFinishedCallback中, 我们只需要重复执行Stepper_UpdateHandler函数就行, 关于这个函数的实现其实很简单, 下面我会说明.
而stepMove函数的具体实现如下:
// todo 以指定速度(频率), 移动指定步数
static void step_move(STEPID id, int32_t tar_steps, uint32_t freq){STEPMOTOR* m = &stepm[id];if(tar_steps == 0 || freq <=0){m->is_finish = 1;return;}m->is_finish = 0; // 刷新完成标志位// todo 停止并复位定时器m->hw->pwmStopIT(id);m->hw->htimStop(id);//todo 方向引脚置位m->dir = (tar_steps >= 0) ? 1 : 0;if(m->dir) m->hw->dirSet(id);else m->hw->dirReset(id);// todo 设置目标步数 并对其进行步数限幅if(m->is_limit_step){int32_t pre_accumulate_step = m->accumulate_step; // 存储上一次的步数m->accumulate_step = (int16_t) LIMIT(m->accumulate_step + tar_steps, m->max_step, m->min_step);m->tar_step = abs( m->accumulate_step - pre_accumulate_step);}else{m->tar_step = tar_steps;}m->cur_step = 0; // 每刷新一次当前步数置为0// todo 限制频率范围 注意符号和取值范围freq = LIMIT((int32_t)freq, (int32_t)m->max_freq, (int32_t)m->min_freq);m->cur_freq = freq; // 获取当前频率// todo 重新设置占空比(占空比永远为比较值的一半, 即50%占空比)m->hw->pwmSetCompare(id, freq);// todo 重新启动定时器m->hw->htimStart(id);// todo 启用更新中断m->hw->pwmStartIT(id);
}
以及中断函数
// todo (重要)定时器更新中断处理(需在stm32f4xx_it.c(或者中断回调函数)中调用)
void Stepper_UpdateHandler(STEPID id) {STEPMOTOR* m = &stepm[id];if(m->cur_step < m->tar_step) {m->cur_step++;} else { // todo 如果检测到当前步数到了目标值就直接PWM_Stop不发波了m->is_finish = 1;m->hw->pwmStopIT(id);}
}
每一步的操作我都敲了注释, 总体上的控制思路如下(这也是比较常用的控制思路):
通过PWM中断去对每一步进行计数存入成员变量cur_step中, 方波每个周期进一次中断, 同时步进电机收到一个脉冲, 电机走一步. 当电机的当前步数大于等于目标步数pwm停止.
电机停止
stepm[STEPID].stop(STEPID);
调用即可, 函数实现仅仅是让目标步数等于当前步数
// todo 立即停止电机
static void step_stop(STEPID id) {STEPMOTOR * m = &stepm[id];//__HAL_TIM_DISABLE_IT(m->hw->htim, TIM_IT_UPDATE);m->tar_step = m->cur_step;
}
设置范围
// id 模式 最大值 最小值
stepm[STEPID].setRange(STEPID, char , int32_t , int32_t );
这里提供了两种模式:
- 'f': 设置速度/频率范围 -- 较为常见
- 's': 设置最大累计步数范围 -- 我控制二维步进云台时用的, 防止云台跑到范围外, 对累计的步数进行限幅
// todo 立即停止电机
static void set_range(STEPID id, char mode, int32_t ma, int32_t mi){STEPMOTOR* m = &stepm[id];switch (mode) {case 'f': // todo 设置频率范围m->max_freq = ma;m->min_freq = mi;break;case 's':m->max_step = ma;m->min_step = mi;break;}
}
是否启用位移限幅
主要是对标志位is_limit_step进行操作, 如果你不需要累计位移控制, 比如说小车的应用场景, 可以置位不再限幅
//todo 调用示例
stepm[STEPID].limitStep(STEPID);
stepm[STEPID].noLimitStep(STEPID);//todo 实现
static void limit_step(STEPID id){stepm[id].is_limit_step = 1;}
static void no_limit_step(STEPID id){stepm[id].is_finish = 0;}