浅析3D打印机原理


3楼猫 发布时间:2022-03-21 18:05:33 作者:ArnoSolo Language

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

开发环境

  • 开发框架
  • Arduino
  • 主控芯片
  • AVR mega2560
  • 主板
  • MKS GENL V2.1
  • 电机驱动
  • X TMC2008
  • Y TMC2008
  • Z A4988
  • E TMC2225
  • 显示器
  • 暂未实现,通过串口交互
  • 机身
  • 大鱼i3

G代码解析

本节的要求是将 G1 X2.4 Y5.6 这样的字符串转换 gcode对象,完成即可进入下一节。
  • 最常见G代码 G1 F200 X2 Y4 ; 移动到(2,4,0) 速度为200mm/min G28 X0 Y0 ; move X/Y to min endstops M104 S200 ; 热端升温到200℃ G代码详解: marlin
  • gcode对象
    示例 gcode.cmdtype = 'G'; gcode.cmdnum = 1; gcode.X = 2.4; gcode.Y = 5.6; gcode.hasX = true; gcode.hasY = true;

SD卡

正常来说,我们需要配置SPI以使用SD卡,但是由于我们的开发框架是Arduino,且我们的芯片是mega2560,所以我们可以直接使用Arduino SD库。我们唯一需要做的就是在主板原理图中找到SD卡的CS引脚(chip select),并调用SD.begin(csPin)

温度控制

本节要求是可以将热端温度控制在200℃。
具体来说就是要创建一个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项则在快到目标温度时萎靡不振。

电机控制

这一节的要求能够控制步进电机正反转。
具体来说,就是需要实现motor对象的以下方法
motor.enable(); motor.setDir(1); // 设定电机转动方向 motor.moveOneStep();
A4988
我们将使用A4988模块来控制步进电机,下面给出A4988的原理图:
  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次运动
  1. ENABLE 接低电平模块开始工作,接高电平则模块关机悬空则模块工作
  2. SLEEP 接低电平则电机断电,用手拧可以自由转动。接高电平则电机工作。
  3. 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 个脉冲

限位开关

打印机每次开机都需要寻找零点,想要归零的话,除了需要了解如何驱动电机外,还需要了解限位开关的原理。我们将需要实现以下方法:
xMin.isTriggered() // 开关被按下则返回true
限位开关有三个引脚分别是常开,常闭,公共端。相应的就有了两种工作模式常开常闭。这里我们选择常闭。于是通过读取MCU引脚电平高低即可实现判断,具体实现在module/Endstop.cpp
限位开关状态电路通断MCU引脚电平未触发通低触发断高。

运动控制

这一节的目标是串口输入 G1 F1000 X6 Y3 热端将到达指定坐标点(6,3,0)。
如何使得轨迹看起来显示一条直线?
假设我们的起始点为(0,0) 那么走到(6, 3)就需要要求 X电机走(6 x 80)步,Y电机走(3 x 80)步。 我们当然可以要求X电机先走,Y电机后走,也能到达目的地,但是画出来的线与理想的线段可就有相当的差距了。或者我们可以先画出理想线段,然后在它的附近画线。
可是这该怎么实现呢?这个问题前人已经想好了,还给它起了个名字叫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; }
注意实现:
不要使用浮点数来计算步数,因为会导致失步。
多个运动指令
上文我们实现了如何执行一条G1指令。那么多条指令该怎么办呢?
我们可以将一个包含了每个电机运动多少步,向那个方向运动的对象放入一个队列(queue)中。需要的时候再从队列中取出。
  • block typedef struct { volatile bool isBusy; volatile bool isReady; volatile bool isDone; bool needRecalculate; uint32_t id; ​ double distance; // mm double stepsPerMm; // steps/mm int8_xyze_t dir; // -1 or 1 int32_xyze_t startStep; // mm uint32_xyze_t steps; // steps uint32_t stepEventCount; // steps uint32_t stepEventCompleted; // steps uint32_t accelerateUntil; uint32_t decelerateAfter; double entrySpeed; // mm/s double exitSpeed; // mm/s double nominalSpeed; // mm/s uint32_t entryRate; // steps/s uint32_t exitRate; // steps/s uint32_t nominalRate; // steps/s uint32_t speedRate; // steps/s ​ double acceleration; // steps/sec^2 uint32_t accelerateRate; // steps/sec^2 } block_t;

速度控制

使用定时器中断的时间来控制打印头前进的速度。
比如我们希望速度是1000steps/s,那么定时器就需要每1ms产生一次,同时在中断服务函数中执行一次步进事件(step event)。
如果我们需要改变速度,则可以在中断服务函数中设定触发中断的计数器值。不过我们现在可以暂时把它设置成匀速。

速度衔接

这一节的目标是计算两运动线段的衔接速度,而后计算出每个运动线段何时加速何时减速。
其实做好上面的步骤,把移动速度设置成匀速,打印机就能用了。但是我们还是能够通过适当的改变移动速度来使得打印机的打印速度有适当的提高。
梯形加速
上文提到我们可以通过改变中断时间来改变速度。那么就会涉及到一个问题:何时加速,何时减速?
具体来说就是将一个block分成加速段,匀速段以及减速段并计算它们的长度。计算并不复杂,已在下图给出,需要注意的是如果当前block长度很短的话,加速图形会由梯形变成三角形。
衔接速度
为了不让每个block之间速度跟连贯。我们需要计算每个block的进入速度和退出速度。估算方法下文已给出,需要注意的是图中的圆弧只是用来估算衔接速度的,打印头实际的路径并不会经过这段圆弧。

后记

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


© 2022 3楼猫 下载APP 站点地图 广告合作:asmrly666@gmail.com