从零编写3D打印机固件

从零编写3D打印机固件浅析3D打印机原理 本文将通过实现一个3D打印机固件来解释3D打印机是如何工作的. 点击文章索引可快速跳转. 代码地址 起因 ​ 大家好, 我是阿诺. 去年我网购了一台3D打印机, 配置很简单, 30

大家好, 我是阿诺. 今天我们将通过实现一个3D打印机固件来理解3D打印机是如何工作的. 代码地址

734C54DF-3237-45DA-BFF8-E3F1156904B4_1646661810834.jpeg

开发环境

  • 开发框架

    • Arduino
  • 主控芯片

  • 主板

  • 电机驱动

    • X TMC2008
    • Y TMC2008
    • Z A4988
    • E TMC2225
  • 显示器

    • 暂未实现, 通过串口交互
  • 机身

B1B972AB-9040-4623-9ED5-C4AAC4668939_1645943088053.jpeg

G代码解析

这一节的目标是实现Gcode::parse方法, 将串口输入的 G1 X2.4 Y5.6 转换gcode对象

gcode.cmdtype = 'G';
gcode.cmdnum = 1;
gcode.x = 2.4;
gcode.y = 5.6;

G代码格式我参考的是marlin. 这部分比较简单, 就略过了, 具体的实现可以看gcode/gcode.cpp. 完成即可进入下一节.

温度控制

这一节的目标是串口输入 M104 S100 热端开始加热, 达到100℃后维持在100℃.

温度控制使用PID控制器, 具体来说就是要创建一个hotend对象,它将拥有一下几个功能:

  • hotend.setTargetTemp(200) 设定目标温度为200℃
  • hotend.readTemp() 读取当前温度
  • hotend.update() 以更新MOS管的开关时间

而后创建一个每200ms执行一次的中断服务函数, 每次中断执行一次hotend.update(). (定时器初始化放在Heater::init中)

其中Heater类的实现在module/Heater.cpp, 中断服务函数在main.cpp

这里主要需要讲的是PID控制器几个参数的含义(虽然它们名字听上去很复杂,但是其实只是简单的加减乘除):

  • 控制器输出值

    • 一个0~255的数, 125表示加热器功率设为50%
  • Error

    • 当前温度150℃, 目标温度200℃, 则偏差值为50
  • Proportion 比例

    p = kp * err;
    
    • 假设当前温度150℃, 目标温度200℃, kp值为1.0, 则p项=50. 加热器功率设为(50/255)=20%
    • 有了这一项就能控制温度. 但是只有这一项, 可能加到170℃温度就加不上去了, 因为这时候加热器功率只有12%, 正好加热器向空气中散发的热量也是这个功率. 这种现象被称为稳态误差.
  • Integration 积分

    pidIntegral += err;
    i = ki * pidIntegral;
    
    • i项可以解决只有p项时出现的稳态误差. 假设ki为0.5, 那么当加到170℃温度就加不上去时, pidIntegral每200ms就会增加30, i项每200ms就会增加15, 加热器功率每200ms就会增加(15/255)=6%, 如此假以时日, 温度自然就上去了.
  • Differentiation 微分

    d = kd * (err - pidPrevErr);
    
    • 你可能会说, 按我这个说法, 那只要有p项i项就能实现温度控制了. 确实如此, 如果你发现有了p项i项就能很好的控制温度的话, 那完全可以把ki设成0. 但是如果我们想要防止温度变化过快的话, 那么可以试试加上d项. 因为假设目标温度为200℃, kd为1. 如果上一个周期温度为170℃, 这一个周期温度为190℃, 那么d项就是20. 而如果上一个周期温度为170℃, 这一个周期温度还是170℃, 那么d项就是0. 可以说d项这家伙就是讨厌变化. 这在到达目标温度后, 防止温度快速滑落很有用, 因为上文不是提到了嘛, i项需要”假以时日”, p项则在快到目标温度时萎靡不振.

电机控制

这一节的目标是实现步进电机的正反转, 实现G28归零指令

image-20220308110755917.png

A4988

我们将使用A4988模块来控制步进电机, 下面给出A4988的原理图

image-20220308111200668.png

  1. VMOT

    接8v~35v直流电源,需要在VMOTGND间布置一个100uf的电容,以快速响应电机的电能需求.

  2. 1A 1B

    接第一个线圈

  3. 2A 2B

    接第二个线圈

  4. VDD

    接MCU电源

  5. DIR

    方向控制引脚,接MCU输出,高低电平分别代表一个转动方向

  6. STEP

    一个方波电机运动一次,如果设置步进细分为1,则运动一次一步进,一次步进为1.8°,200步可以转一圈

  7. MS1 MS2 MS3

    对步进进行细分,至多可以将一步进细分为16次运动

1297124-20191219111530730-509408618.png 8. ENABLE

低电平模块开始工作, 接高电平则模块关机, 悬空则模块工作.

  1. SLEEP

    接低电平则电机断电,用手拧可以自由转动. 接高电平则电机工作.

  2. RESET

    默认悬空. 收到低电平时,重置模块. 如果不打算控制这个引脚,则应该将其连接到SLEEP引脚以设置为高电平.

所以使用A4988控制电机一共有4步, 具体实现在module/Stepper.cpp

  1. 接线. 前往注意不要装反了,装反了模块会烧掉.
  2. 设置enable引脚为低电平以激活模块
  3. 设置dir引脚以设置方向
  4. step引脚发射脉冲以要求电机运动
轴步数

现在我们知道了如何经由A4988控制电机, 但是电机转一步(step), 打印头到底走多少距离(mm)呢?

  • 同步轮与皮带

    以2GT, 20齿的同步轮为例. 2GT的意思是走一个齿皮带运动2mm, 那么如果同步轮有20齿, 转一圈皮带走40mm. 而如果我们电机驱动采用16细分, 那么步进电机一圈就是3200步.

    轴步数 = 3200steps / 40mm = 80steps/mm
    

    所以如果我们使用i3的结构, 希望打印头在x轴正方向上前进10mm, 那么就需要MCU向A4988发射 3200 * 10 = 32000 个脉冲.

  • 丝杆

    以螺距2mm, 导程8mm的丝杆为例. 导程的意思是丝杆转一圈所行走的直线距离. 所以

    轴步数 = 3200steps / 8mm = 400steps/mm
    

    所以如果我们使用i3的结构, 希望打印头在z轴正方向上前进10mm, 那么就需要MCU向A4988发射 3200 * 400 = 1,280,000 个脉冲

G28归零

想要归零的话, 除了需要了解如何驱动电机外, 还需要了解限位开关的原理

image-20220308113843629.png 限位开关有三个引脚分别是常开,常闭,公共端. 相应的就有了两种工作模式常开常闭. 这里我们选择常闭. 于是通过读取MCU引脚电平高低即可实现判断, 具体实现在module/Endstop.cpp

限位开关状态 电路通断 MCU引脚电平
未触发
触发

路径规划与执行

这一节的目标是串口输入 G1 F1000 X6 Y3 热端将到达指定坐标点.

前进方法

假设我们的起始点为(0,0) 那么走到(6, 3)就需要要求 X电机走(6 x 80)步, Y电机走(3 x 80)步. 我们当然可以要求X电机先走, Y电机后走, 也能到达目的地, 但是画出来的线与理想的线段可就有相当的差距了. 或者我们可以先画出理想线段, 然后在它的附近画线.

052B3E34-1BB4-4F72-92F0-9E9F1A33BFCE_1646712370349.jpeg 可是这该怎么实现呢? 这个问题前人已经想好了, 还给它起了个名字叫Bresenham算法. 具体来说就是既然X方向需要走480步, Y方向需要走240步, 那么就相当于总共要走480次, X方向每次前进一步, Y方向每2次前进一步. 这480次运动事件, 每一次被称为一个step event. 总的次数叫做step event count, 它的值就是X,Y中的较大值.

// module/Planner.cpp - planBufferLine
block.stepEventCount = getMax(block.steps);

// main.cpp - motion control isr
motorX.deltaError = -(curBlock->stepEventCount / 2);
motorY.deltaError = motorX.deltaError;

motorX.deltaError += curBlock->steps.x;
if (motorX.deltaError > 0) {
    motorX.moveOneStep();
    motorX.deltaError -= curBlock->stepEventCount;
}

motorY.deltaError += curBlock->steps.y;
if (motorY.deltaError > 0) {
    motorY.moveOneStep();
    motorY.posInSteps += curBlock->dir.y;
    motorY.deltaError -= curBlock->stepEventCount;
}

注意实现:

​ 不要使用浮点数来计算步数, 因为会导致失步

16467305795311.png

速度控制

使用定时器中断的时间来控制打印头前进的速度.

比如我们希望速度是1000steps/s, 那么定时器就需要每1ms产生一次, 同时在中断服务函数中执行一次步进事件(step event).

如果我们需要改变速度, 则可以在中断服务函数中设定触发中断的计数器值. 不过我们现在可以暂时把它设置成匀速.

多个运动指令

上文我们实现了如何执行一条G1指令. 那么多条指令该怎么办呢?

我们可以将一个包含了每个电机运动多少步, 向那个方向运动的对象放入一个队列(queue)中. 需要的时候再从队列中取出.

速度衔接

这一节的目标是计算两运动线段的衔接速度, 而后计算出每个运动线段何时加速何时减速.

其实做好上面的步骤, 把移动速度设置成匀速, 打印机就能用了. 但是我们还是能够通过适当的改变移动速度来使得打印机的打印速度有适当的提高.

梯形加速

上文提到我们可以通过改变中断时间来改变速度. 那么就会涉及到一个问题: 何时加速, 何时减速?

具体来说就是将一个block分成加速段,匀速段以及减速段并计算它们的长度. 计算并不复杂, 已在下图给出, 需要注意的是如果当前block长度很短的话, 加速图形会由梯形变成三角形.

9D04FC9D-22CA-49D8-B0CE-71BEA0E9D407_1646716173573.jpeg

衔接速度

为了不让每个block之间速度跟连贯. 我们需要计算每个block的进入速度和退出速度. 估算方法下文已给出, 需要注意的是图中的圆弧只是用来估算衔接速度的, 打印头实际的路径并不会经过这段圆弧.

E363297E-06F7-476F-98B6-389808AB72BA_1646717676589.jpeg

后记

我以前是不喜欢旅行的, 因为在我看来那不过是换个背景拍照. 我现在喜欢了, 因为我对旅行的定义变了, 我现在把它定义成对一个事物的深入探索, 而了解3D打印机是如何工作的就是其一.

今天的文章从零编写3D打印机固件分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21780.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注