很長時間沒有寫博客,因為各種各樣的事情占去大塊時間,只有零碎時間偶爾在CSDN逛逛也偶爾回幾個帖子。很久以前就看到一些光驅DIY雕刻機之類的,很是向往,最近這幾天得閑就TB了一套Arduino UNO R3實驗套件,也實踐了一番這種單任務平台,從點亮個LED翻到步進電機就再也忍不住了,於是狠狠的操作了一番ULN2003和28BYJ48,好在經過對度娘的一番拷問,了解了不少東西,基本了解了各種所需的基本知識,其中比較需要自己實驗的方面列出來,以供大家參考:
一、步進電機、驅動和接線
買了兩個L298N來驅動光驅步進電機,其中in1-in4就是接Arduino程序里面定義的引腳1-4,電源采用12V供電。我拆的兩個光驅電機(拆了3個其中一個是無刷的),具體接法可以自己試一試,連接兩個引腳之后轉動有阻力的就是一組,有萬用表測一下也可以的。我這兩個光驅電機都是依次排列的,即12、34兩組,所以接的時候依次接L298N的ou1-ou4即可。唯一需要注意的地方就是L298N的GND接到Arduino的GND上。
二、給Arduino編程
用c寫代碼還是蠻別扭的,就像你開了這么多年自動擋,讓你開手動檔能不能走起呢?當然能,就是得光想着離合那點事。這里主要考慮的就是繪圖的話那2k的內存不好辦,所以還得上位機控制,所以整個程序框架建立的時候就是服從上位機指令;然后就是電機工作的平穩性,如果電機帶着整個機械機構在那廣場舞,那激光指不定射誰一眼呢(雖然我打算綁只圓珠筆搞定),所以平穩還是要的,也就修改了電機庫使它從4拍變為8拍並且增加了S型加速。所以說,驅動細分不細分的,軟件實現就可以了,畢竟我們的要求不高,2k內存還是綽綽有余的。
1、與上位機通訊
發送消息很簡單:
//請求數據 void RequestData() { Serial.println(r_RequestData); delayMicroseconds(2000); //16000000/96000=1666.66666 }
注意別粘包了就好。而且發送的字符盡量少,1個字節可以表示二百五還多的命令很夠用了。
接收消息也不難,分類處理一下就可以了:
//板子初始化操作后進入的循環執行函數。 void loop() { if (Serial.available()!=0) { msgLen = Serial.readBytes(msgBuff, msgBuffSize); //讀取消息 if (msgLen > 0) { CommandParsing(msgBuff); //處理消息 } }else{ RequestData(); //請求數據 } } void CommandParsing(char buff[]) { if (buff[0] == c_Stop) { DoStop(); }else if (buff[0] == c_xForward || buff[0]==c_xBackOff) { DoxMove(buff[0], ToInt32(buff)); }else if (buff[0] == c_yForward || buff[0] == c_yBackOff) { DoyMove(buff[0], ToInt32(buff)); }else if (buff[0] == c_zUp || buff[0] == c_zDown) { DozMove(buff[0]); }else if (buff[0] == c_xSpeed) { xSpeed = ToFloat(buff); Serial.println(xSpeed); xStepper.setSpeed(xSpeed); }else if (buff[0] == c_ySpeed) { ySpeed = ToFloat(buff); yStepper.setSpeed(ySpeed); }else { Serial.println(r_UnknownCommand); } memset(msgBuff, 0, msgBuffSize); }
我的接收緩沖區只有5字節,也就是說每個命令都一樣長——5字節,這可以讓代碼簡單一些,看了一下Serial的源碼,里面設置了接收緩沖區64字節,所以上位機可以一口氣發10個命令,再多就被舍棄了,還是等待下位機請求之后再發才保險。
2、修改Stepper庫
絕大部分還是保留原來的內容,只是稍作修改,在Stepper_CDROM.h中:
const int number_of_steps=8; // total number of steps this motor can take unsigned long CurDelay; unsigned long GetSModelLine(int StepCount,int StepLeft); float sSpeed[8] = {16.6667, 7.1429, 4.1667, 2.5, 1.6667, 1.3158, 1.1628, 1.0638};
把原來構造函數的第一個參數修改為常量8,后面也跟着修改了setSpeed函數的實現:
//設置每分鍾轉多少步 void Stepper_CDROM::setSpeed(float whatSpeed) { this->step_delay = 60L * 1000L * 1000L / this->number_of_steps / whatSpeed; }
就如注釋的一樣,設置的速度是每分鍾的步數而不是轉數,其實這個值最后還是要不斷的調試得出,我最后確定了用這樣一個數值:
float ySpeed = 4864; //y軸步進電機每分鍾步數的最大值
而和原來代碼格格不入的變量名就是我搞出來的了,為了計算S型加速曲線,實際上只有8步速或減速過程,這些數值是最高速時的時間間隔的倍數。雖然這樣做要比Exp函數(S型曲線的原函數計算非常耗時)來的快的多得多,但是由於我沒有這方面的經驗,所以步數和加速度可能很不理想。但是無論如何,我修改了原來代碼的內容,使用了我的時間間隔來代替原有間隔,做為萌新看起來可能還不錯:
void Stepper_CDROM::step(int steps_to_move) { int steps_left; if (steps_to_move > 0) { this->direction = 1; steps_left = steps_to_move; } if (steps_to_move < 0) { this->direction = 0; steps_left = -steps_to_move; } int StepCount = steps_left; this->CurDelay = GetSModelLine(StepCount,steps_to_move); // 到達延遲時間后轉動一步,直到轉動全部步數。 while (steps_left > 0) { unsigned long now = micros(); // 計算延遲是否到達 if (now - this->last_step_time >= CurDelay) { // 記錄本次轉動時間: this->last_step_time = now; // 根據方向設置當前拍: if (this->direction == 1) { this->step_number++; if (this->step_number == this->number_of_steps) { this->step_number = 0; } } else { if (this->step_number == 0) { this->step_number = this->number_of_steps; } this->step_number--; } // 記錄剩余步數: steps_left--; // 運行電機 stepMotor(this->step_number % 8); //計算下一個延遲 this->CurDelay = GetSModelLine(StepCount, steps_left); } } } unsigned long Stepper_CDROM::GetSModelLine(int StepCount,int StepLeft) { if (StepCount < 16) { return this->step_delay*2; //不足16步則無法完成一次加速和一次減速,以1/2最高速度運行。 } if (StepLeft <= 8) { return this->step_delay * this->sSpeed[StepLeft-1]; //最后8拍倒序執行即減速。 } if (StepCount - StepLeft < 8) { return this->step_delay * this->sSpeed[StepCount - StepLeft]; //前8拍順序執行即加速。 } return this->step_delay; //前后8拍之間的最高速度運行。 }
紅的的行就是應用我的時間間隔的地方了,而下面的自定義函數就是計算過程,這只需要查表就可以了,當然,我懶到對於不能進行完整加減速的過程簡單粗暴的用了一個半速。但無論如何,經過各種調試,現在這個光驅里拆出來的架子上面的機械機構運行時速度很快,聲音很小;當然,還有更重要的一點,我測試的結果是500拍就差不多從一端走到另一端共3.8cm、12圈多一點,所以這個電機大約是40步轉一圈,細分8拍之后,每一拍大約0.076mm,這個精度也可以了,但是如果用原來的庫進行4拍驅動就只能達到0.152的精度,走6拍差不多1mm了,很明顯的鋸齒有木有。之前上傳的程序有一點問題,已經修正了。下面添加一個光驅電機8拍的順序:
1000、1100、0100、0110、0010、0011、0001、1001。1表示高電平,0表示低電平。
這幾天又看了看A4988驅動,准備入手兩三塊,這個驅動編寫程序要簡單很多。
3、上位機程序
這下開上自動擋的趕腳又回來了,很簡單的封一個類就可以:
Private Enum Command As Byte c_Stop = 255 '暫停 c_Continue = 254 '繼續 c_xForward = 1 'x軸前進 c_xBackOff = 2 'x軸后退 c_yForward = 3 'y軸前進 c_yBackOff = 4 'y軸后退 c_zUp = 5 'z軸抬起 c_zDown = 6 'z軸落下 c_xSpeed = 7 'x軸速度 c_ySpeed = 8 'y軸速度 End Enum Private Enum Request As Byte r_UnknownCommand = Asc("d") '未知命令 r_RequestData = Asc("e") '請求數據 End Enum Private WithEvents mPort As SerialPort Event RequestData(msg As String) Sub New(PortName As String, BaudRate As Integer) mPort = New SerialPort(PortName) mPort.BaudRate = BaudRate Try mPort.Open() Catch ex As Exception MsgBox(ex.ToString) End Try End Sub Private Sub mPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles mPort.DataReceived Dim inData As String = CType(sender, SerialPort).ReadLine.TrimEnd({CChar(vbCr), CChar(vbLf)}) RaiseEvent RequestData(inData) End Sub Private Sub mPort_Disposed(sender As Object, e As EventArgs) Handles mPort.Disposed Try If mPort IsNot Nothing AndAlso mPort.IsOpen Then mPort.Close() End If Catch ex As Exception End Try End Sub Sub SendCommand_Stop() WritePort({Command.c_Stop, 0, 0, 0, 0}) End Sub
‘此處省略其他函數封裝。
Private Sub WritePort(buff() As Byte)
Try
mPort.Write(buff, 0, 5)
Catch ex As Exception
MsgBox(ex.ToString)
End Try
End Sub
使用指定的端口名和波特率初始化一下端口類,定義命令和請求,然后封裝好不同的命令和請求就可以了,當然這里請求處理還不完整也為了測試方便,把請求事件那里的條件去掉了,但並不影響對整個程序結構的理解。
雖然我的上位機程序對通訊部分進行了很多修改,但是整個框架還是這樣的。添加了一些簡單的功能,但是有些部分還不是很成熟,把經過測試沒有問題的部分說明一下:
3.1圖像的簡單處理:
我使用了OPENCV組件(NUGET)來完成這樣幾個工作:
A、讀取和顯示圖片
Dim ofd As New OpenFileDialog Try ofd.Filter = "jpg files|*.jpg|png files|*.png|bmp files|*.bmp|all files|*.*" ofd.Multiselect = False If ofd.ShowDialog = DialogResult.OK Then ImgSrc = New Mat(ofd.FileName) pnlSrc.BackgroundImage = Mat2Img(ImgSrc) End If Catch ex As Exception MsgBox(ex.ToString) End Try
這個程序很簡單,就是讀取文件然后顯示在Panel上。其中Mat2Img函數如下:
Private Function Mat2Img(mat As Mat) As Bitmap Dim ms As New MemoryStream mat.WriteToStream(ms) Dim result As Bitmap = Bitmap.FromStream(ms) ms.Close() Return result End Function
就是利用內存流轉儲一下而已。
B、灰度化、二值化、邊緣查找
ImgGray = ImgSrc.CvtColor(ColorConversionCodes.BGR2GRAY)
ImgBinary = ImgGray.Threshold(nudThreshValue.Value, nudThreshMaxValue.Value, cmb2ThresholdTypes)
Dim pss()() = ImgBinary.FindContoursAsArray(cmb2RetrievalModes, cmb2ContourApproximationModes)
這樣就可以得到邊緣的點坐標集合,並且這些點是相連的,所以可以優化繪制代碼——按曲線繪制,比一行一行掃描看起來高大上一些。
C、二值圖也按曲線繪制
這個功能還是利用我所熟悉的快速種子填充算法來做,只是Mat類獲取點顏色的方法不太一樣:
Private Function FindRegionByPoint(img As Mat, mTable(,) As Boolean, rect As Rect, p As Point, color As Byte) As Point() Dim result As New List(Of Point) Dim mArray As New Queue '棧——將處理點表 Dim mP As Point = p '正在處理點 Dim mAP As Point '臨時變量——可能被入棧的點 mArray.Enqueue(mP) '入棧 Do If mTable(mP.X, mP.Y) = False Then '若未處理過 If img.At(Of Byte)(mP.Y, mP.X) = color Then '相同顏色則添加臨近點 mAP = New Point(mP.X, mP.Y - 1) '臨近點入棧 If rect.Contains(mAP) AndAlso mTable(mAP.X, mAP.Y) = False Then mArray.Enqueue(mAP) mAP = New Point(mP.X, mP.Y + 1) If rect.Contains(mAP) AndAlso mTable(mAP.X, mAP.Y) = False Then mArray.Enqueue(mAP) mAP = New Point(mP.X - 1, mP.Y) If rect.Contains(mAP) AndAlso mTable(mAP.X, mAP.Y) = False Then mArray.Enqueue(mAP) mAP = New Point(mP.X + 1, mP.Y) If rect.Contains(mAP) AndAlso mTable(mAP.X, mAP.Y) = False Then mArray.Enqueue(mAP) result.Add(New Point(mP.X, mP.Y)) End If mTable(mP.X, mP.Y) = True '修改為已處理 End If If mArray.Count = 0 Then Exit Do Else mP = mArray.Dequeue '出棧 Loop Return result.ToArray End Function
這個函數中使用了一個加速表,因為這個函數只是從一個點來查找一片點,有多個相連的點集的時候需要重復調用,所以加速表是重復使用的,當然以前做過一些簡單的測試,用bitarray來實現速度還要快一點。至於遍歷整個圖像的函數就不貼了,兩層循環而已,沒意思。
以上就是這些天做的一些努力,當然了,今天100大洋的激光頭也到貨了,插在兩個光驅做的框架上做了一下測試,也遇到一些問題。激光器模組的散熱套連個硅脂都不送,貼合不緊導熱不好,在現在低負荷運行的條件下根本不發熱,也就沒處理;接的電源過了ULN2003之后電壓降很多,又沒有合適的變壓器,索性直接插了Ardiuno,所以電流很低,和A4988一起入手一塊恆壓恆流模塊准備給它供電,這樣就可以功率調節到合適的程度,當然散熱器上帶個小風扇是一定的。后面再利用打印機的字車擴大一下雕刻范圍,也許還會買個光軸做個大一點的。