淺析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