记录一次使用面向对象的C语言封装步进电机驱动

简介

(2025/4/21)

        

        本库对目前仅针对TB6600驱动下的42步进电机的基础功能进行了一定的封装, 也是我初次尝试以面向对象的思想去编写嵌入式代码, 和直流电机的驱动步骤相似在调用stepmotor_attach()函数和stepmotor_init()函数之后仅通过结构体数组stepm然后指定枚举变量中的id即可完成对步进电机的基础操作, 其中最核心的是控制函数step_move的实现, 该函数可以在开环状态下指定步进电机的步数频率(速度)进行控制, 后续可能会更新一些经典的控制模型如梯形加减速.
先贴一下项目代码
项目源码

cubemx 配置

如果要使用此库, 你只需要在cubemx中完成以下配置:

  1. 打开定时器的PWM通道并将 Prescaler设置为83(我的时钟主频为84分频后为1MHz, 这个并不是一定得是1MHz, 后面会说)

  2. 打开NVIC

  3. 配置相关的GPIO, 如方向引脚, 这个很简单我就不贴图了

移植

如果你的硬件平台和我一样, 无脑粘贴就行, 如果不一样, 则需要修改相关代码

  1. 修改类型(如果不是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.cstep_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;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/77901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[创业之路-376]:企业法务 - 创业,不同的企业形态,个人承担的风险、收益、税费、成本不同

在企业法务领域&#xff0c;创业时选择不同的企业形态&#xff0c;个人在风险承担、收益分配、税费负担及运营成本方面存在显著差异。以下从个人独资企业、合伙企业、有限责任公司、股份有限公司四种常见形态展开分析&#xff1a; 一、个人承担的风险 个人独资企业 风险类型&…

GNOME桌面隐藏回收站和分区

dconf-editor 搜索 trash&#xff0c;关闭 show-trash 搜索 volumes&#xff0c;关闭 show-volumns

准确--Tomcat更换证书

具体意思是&#xff1a; Starting Coyote HTTP/1.1 on http-8080: HTTP 连接器&#xff08;端口 8080&#xff09;启动成功了。严重: Failed to load keystore type PKCS12 with path conf/jlksearch.fzsmk.cn.pfx due to failed to decrypt safe contents entry: javax.crypt…

禁止ubuntu自动更新

由于ubuntu server和desktop版本都默认 启动了&#xff0c;自动更新内核的操作。这对于生 产环境来说是不友好的。容易导致亿赛通 无法启动 默认开启了内核自动更新所以我们关闭自 动内核更新。 1.禁止更新执行 sudo apt-mark hold linux-image-generic linux-headers-generic…

vue3 + element-plus中el-drawer抽屉滚动条回到顶部

el-drawer抽屉滚动条回到顶部 <script setup lang"ts" name"PerformanceLogQuery"> import { ref, nextTick } from "vue"; ...... // 详情 import { performanceLogQueryByIdService } from "/api/performanceLog"; const onD…

【重走C++学习之路】16、AVL树

目录 一、概念 二、AVL树的模拟实现 2.1 AVL树节点定义 2.2 AVL树的基本结构 2.3 AVL树的插入 1. 插入步骤 2. 调节平衡因子 3. 旋转处理 4. 开始插入 2.4 AVL树的查找 2.5 AVL树的删除 1. 删除步骤 2. 调节平衡因子 3. 旋转处理 4. 开始删除 结语 一、概念 …

char32_t、char16_t、wchar_t 用于 c++ 语言里存储 unicode 编码的字符,给出它们的具体定义

&#xff08;1&#xff09; #include <iostream> #include <string>int main() { std::u16string s u"C11 引入 char16_t"; // 定义 UTF-16 字符串for (char16_t c : s) // 遍历输出每个 char16_t 的值std::cout << std::hex << (…

redis数据类型-基数统计HyperLogLog

redis数据类型-基数统计HyperLogLog 文档 redis单机安装redis常用的五种数据类型redis数据类型-位图bitmap 说明 官网操作命令指南页面&#xff1a;https://redis.io/docs/latest/commands/?nameget&groupstringHyperLogLog介绍页面&#xff1a;https://redis.io/docs…

逻辑思维:从混沌到秩序的理性推演在软件开发中的应用

引言 在软件开发的过程中&#xff0c;逻辑思维就像是开发者的“GPS导航”&#xff0c;帮助我们从混沌的需求中找到清晰的解决方案。想象一下&#xff0c;如果没有逻辑思维&#xff0c;我们可能会在需求的海洋中迷失方向&#xff0c;最终写出一堆“看似聪明但毫无意义”的代码。…

Spring AI Alibaba Graph基于 ReAct Agent 的天气预报查询系统

1、在本示例中&#xff0c;我们仅为 Agent 绑定了一个天气查询服务&#xff0c;接收到用户的天气查询服务后&#xff0c;流程会在 AgentNode 和 ToolNode 之间循环执行&#xff0c;直到完成用户指令。示例中判断指令完成的条件&#xff08;即 ReAct 结束条件&#xff09;也很简…

HCIP(综合实验2)

1.实验拓补图 2.实验要求 1.根据提供材料划分VLAN以及IP地址&#xff0c;PC1/PC2属于生产一部员工划分VLAN10,PC3属于生产二部划分VLAN20 2.HJ-1HJ-2交换机需要配置链路聚合以保证业务数据访问的高带宽需求 3.VLAN的放通遵循最小VLAN透传原则 4.配置MSTP生成树解决二层环路问题…

使用 rebase 轻松管理主干分支

前言 最近遇到一个技术团队的 dev 环境分支错乱&#xff0c;因为是多人合作大家各自提交信息&#xff0c;导致出现很多交叉合并记录&#xff0c;让对应 log 看起来非常混乱&#xff0c;难以阅读。 举例说明 假设我们有一个项目&#xff0c;最初develop分支有 3 个提交记录&a…

使用openssl为localhost创建自签名

文章目录 自签名生成命令安装安装证书浏览器证书管理器 自签名 生成命令 使用openssl生成私钥和证书。 openssl req -x509 -newkey rsa:4096 -nodes -days 365 -subj "/CNlocalhost" -addext "subjectAltNameDNS:localhost" -keyout cert.key -out cer…

AI编程助手Cline之快速介绍

Cline 是一款深度集成在 Visual Studio Code&#xff08;VSCode&#xff09; 中的开源 AI 编程助手插件&#xff0c;旨在通过结合大语言模型&#xff08;如 Claude 3.5 Sonnet、DeepSeek V3、Google Gemini 等&#xff09;和工具链&#xff0c;为开发者提供自动化任务执行、智能…

1.微服务拆分与通信模式

目录 一、微服务拆分原则与策略 业务驱动拆分方法论 • DDD&#xff08;领域驱动设计&#xff09;中的限界上下文划分 • 业务功能正交性评估&#xff08;高内聚、低耦合&#xff09; 技术架构拆分策略 • 数据层拆分&#xff08;垂直分库 vs 水平分表&#xff09; • 服务粒…

Element Plus表格组件深度解析:构建高性能企业级数据视图

一、架构设计与核心能力 Element Plus的表格组件&#xff08;el-table&#xff09;基于Vue 3的响应式系统构建&#xff0c;通过声明式配置实现复杂数据渲染。其核心设计理念体现在三个层级&#xff1a; 数据驱动&#xff1a;通过data属性绑定数据源&#xff0c;支持动态更新与…

07前端项目----面包屑

面包屑 效果实现代码全局事件总线-$bus 效果 实现代码 上节searchParams中参数categoryName是表示一二三级分类所点击的列表名 <!--bread面包屑--> <div class"bread"><ul class"fl sui-breadcrumb"><li><a href"#"…

kafka jdbc connector适配kadb数据实时同步

测试结论 源端增量获取方式包括&#xff1a;bulk、incrementing、timestamp、incrementingtimestamp&#xff08;混合&#xff09;&#xff0c;各种方式说明如下&#xff1a; bulk: 一次同步整个表的数据 incrementing: 使用严格的自增列标识增量数据。不支持对旧数据的更新…

基于Hadoop的音乐推荐系统(源码+lw+部署文档+讲解),源码可白嫖!

摘要 本毕业生数据分析与可视化系统采用B/S架构&#xff0c;数据库是MySQL&#xff0c;网站的搭建与开发采用了先进的Java语言、爬虫技术进行编写&#xff0c;使用了Spring Boot框架。该系统从两个对象&#xff1a;由管理员和用户来对系统进行设计构建。主要功能包括&#xff…

CentOS的安装以及网络配置

CentOS的下载 在学习docker之前&#xff0c;我们需要知道的就是docker是运行在Linux内核之上的&#xff0c;所以我们需要Linux环境的操作系统&#xff0c;当然了你也可以选择安装ubuntu等操作系统&#xff0c;如果你不想在本机安装的话还可以考虑买阿里或者华为的云服务器&…