這篇文章約為2600字,撰寫時間為120分鐘。
特別感謝:BI6OPR 提供的方案與原始程式
本文遵循CC BY-NC-SA 4.0協議,保留著作權。
文章開始!
前言
我正在籌建自己的射電天文望遠鏡,拋物面已經準備好,但發現轉動器竟然非常昂貴。例如Yaesu的G5500,一個就要數千元,實在無法負擔這樣的費用。在圈內一些大師的指引下,我發現了一款被拆解過的、能夠承受高負載的步進雲台,並了解到BI6OPR的大師已經初步將其應用於Pelco-D協議,但還不夠完善。由於我對程式設計有一些了解,並參考了大師和網上的資料,最終在BI6OPR提供的原始程式上完成了相對滿意的Pelco-D解碼。這篇文章的目的是分享一些我的淺見,希望能夠為各位朋友提供一些相關的想法。
硬體方面
作者考量自身的能力最終放棄了STM32,而是選擇了更適合於物聯網(IoT)的ESP32系列晶片。基本思路是透過ESP32晶片的IO引腳控制雲台方向,以及使用PWM調速,並利用ESP32晶片的TTL串行介面與電腦建立通訊,接收轉向指令,並將轉向狀態傳送給電腦端。步進馬達驅動器使用了24V交流大功率驅動板。
請注意:本文主要介紹韌體代碼的部分內容,關於硬體部分,請參閱 GitHub。
程式碼實現
由於採用了 ESP32 系列晶片,因此在 MicroPython 和 C++ 中選擇使用 C++ 進行開發。(當然,這並非因為新的 Pycharm 無法與 MicroPython 協同工作所導致的放棄。)。這個 IDE 使用了我們常用的 VS Code,搭配 PlatformIO,開發起來還是比較順手的。
我們現在在程式碼的開頭導入 Arduino 函式庫,以便後續呼叫:
包含 <Arduino.h>
ESP32 是一種單片機,透過迴圈來執行相應的程式任務,也就是在 `loop()` 函數中的程式碼。基本思路是不斷地循環讀取串接埠(Serial Port)上的資料,若讀到相應的命令,則進行轉向控制後繼續讀取串接埠上的資料;若串接埠發送停止旋轉指令或新的指令,則執行新的指令。
首先,讓我們在 `setup()` 函數中,對單片機啟動時進行初始化。
設定波特率為 9600,用於串列通訊:
Serial.begin(9600);
初始化主機板上的 LED 指示燈,以及與控制步進馬達控制板相關的 I/O 引腳:
// 設定 I/O 輸入輸出
const int AZ_DIRECTION_PIN = 26;
const int EL_DIRECTION_PIN = 27;
將 LED\_MSG\_PIN 設定為輸出模式。
將 AZ\_DIRECTION\_PIN 設定為輸出模式。
將 EL\_DIRECTION\_PIN 設定為輸出模式。
將 `AZ_SPEED_PUL_PIN` 設定為輸出模式。
將 EL\_SPEED\_PUL\_PIN 設定為輸出模式。
// 初始化信號燈
digitalWrite(LED\_MSG\_PIN, LOW);
初始化用於控制雲台轉速的硬體級 PWM 輸出埠:
const int PWM\_FREQ = 10000; // PWM 頻率 (單位:赫茲)
const int PWM\_RESOLUTION = 8; // 解析度 (以 8 位表示時,佔空比範圍為 0-255)
const int AZ_SPEED_PUL_PIN = 25;
const int EL_SPEED_PUL_PIN = 14;
ledcSetup(0 , PWM\_FREQ, 8);
ledcAttachPin(AZ\_SPEED\_PUL\_PIN, 0);
ledcSetup(1 , PWM\_FREQ, 8);
設定 LED\_SPEED\_PUL\_PIN 連接引腳為 1。
數據的讀取:
在 `setup()` 初始化後,我們就可以開始接收來自串接埠的 Pelco-D 命令。
我們定義一個新的函數,名為讀取序列資料,用於讀取串列數據。
請參閱Pelco-D 的開發手冊,注意標準 Pelco-D 訊息由 7 個字節的十六進位資料組成,其中需要使用兩個資料位以及最後一個檢查位。
我們先使用程式碼讀取 7 個字節的資料:
int rlen = Serial.readBytes(buf, 7);
然後,利用檢查碼驗證數據是否正確。如果正確,則進行下一步的拆解處理:
如果 (rlen 等於 7) && buf\[0] 等於 0xFF && 如果 buf[1] 等於 0x01
為了確保相同數據能夠進行多次解析執行,還需要加入判斷數據與上次讀取時是否不同,若不同,則再次執行。若沒有這個判斷,可能會導致每次只處理一張卡的問題:
如果 (command != currentCommand) // 僅在收到新的指令時處理
{
處理Pelco D 命令 (command);
currentCommand = command; // 更新當前指令
}
在此,數據的讀取與解析已完成。
數據的執行:
我們定義一個函數處理Pelco D 命令 (整數 `command`)用於解析讀取到的數據,並對步進馬達驅動板進行控制。
其中傳入的 `int` 類型 command 變數是文中分解得到的最終控制命令。Pelco-D 協議的基本轉向命令分為停止、上、下、左、右,分別對應數據 0、2、4、8、16,當然還有「左上」、「右下」等等,這裡就不再詳細說明了。
我們先建立一個索引 (實際上,官方名稱可能不是這個)。已經習慣了這樣稱呼)切換 (指令)將所有指令都儲存在這個索引中。
接下來,我們以「向右轉」為例,首先執行對應的指令位 2。然後,在索引中寫入:
案例二:
這表示當輸入的 `command` 變數為 2 時,會執行接下來的代码區塊。
接下來,我們將控制水平方向的Az驅動電路的腳位設定為高電平:
digitalWrite(AZ\_DIRECTION\_PIN, HIGH);
並設定 PWM 調速命令執行的布林值為 True:
is\_azcontrol\_stepper = true;
這樣做才能讓馬達知道該以什麼速度旋轉。上述兩者必須都存在。
這個布林變數會在下一章的脈衝寬度調變(PWM)控制中被提及。
因此,控制右轉的完整程式碼如下:
案例 2:// 向右轉
digitalWrite(AZ\_DIRECTION\_PIN, HIGH);
is\_azcontrol\_stepper = true;
Serial.println("0002。");
其中 Serial.println("0002。");為了將偵錯數據輸出到串口,以便進行程式的偵錯,可以選擇不寫。
`loop()`函數和脈衝寬度調變 (PWM) 調速:
正如我們在上一段所述,ESP32 系列晶片是透過執行特定程式碼區塊來達到某種目的。
我們希望讓上述函數得以執行,因此必須將它們加入到 `loop()` 函數中。(不包括 setup() 函數),否則它們將無法執行。
我們在主程式中建立 loop() 函數:
void loop()
{
……在此處輸入您的程式碼或函數……
}
首先,將我們先前寫過的讀取串口資料的函數放入:
讀取序列資料。
然後需要判斷先前所述 PWM 調速的布林變數是否為 True。 如果為 True,則啟動 PWM 調速 (這裡可以另外定義一個函數)。我只是直接把 `loop()` 函數刪掉了。):
如果 `is_azcontrol_stepper` 為真
{
ledcWrite(0, 128);
}
如果 `is_azcontrol_stepper` 的值為 `false`
{
ledcWrite(0, 0);
}
與此類似,在垂直方向上的 El 電機。
解釋一下 `ledcWrite(int a, int b)` 的功能。這段程式碼中,「int a」代表 PWM 輸出通道,而在之前的「setup()」函數中,我們將第 0 通道指向第 25 號 I/O 引腳,因此它會在第 25 號引腳輸出方波 PWM 訊號。而「int b」則代表輸出訊號的頻率,透過調整這個變數可以控制相應馬達的轉動速度,0 代表停止。
最後,將透過串接埠傳送目前運作狀態的函數放在 `loop()` 內。
顯示狀態。
這個函數的詳細說明將在下一章中呈現。
為了避免這個函數阻塞串口和程式,我們為它添加了一個計時器,以便定期執行。
如果 (當前的毫秒數 - 之前的毫秒數) >= 間隔
{
previousMillis = currentMillis;
顯示狀態。
}
狀態回傳函數:
為了能在電腦上順暢地瞭解雲台的運動狀態,我們可以透過串接埠傳輸雲台相關的狀態。
我們定義一個顯示狀態。函數,用於儲存狀態傳回的程式碼區塊。
其中,程式碼會將目前的狀態透過串口輸出。不過,以下不再詳細說明:
`Serial.print("目前狀態 - AZ 方向:");`
Serial.print(az_stepper_direction ? "向前" : "向後");
序列輸出:", EL 方向:
Serial.print(el_stepper_direction ? "向前" : "向後");
序列輸出:", AZ 控制:
Serial.print(is_azcontrol_stepper ? "開啟" : "關閉");
序列輸出:", EL 控制:
Serial.println(is_elcontrol_stepper ? "開啟" : "關閉");
具體包含目前雲台的旋轉狀態,以及其轉向方向。
進一步:透過 Wi-Fi 控制
為了與 DTrac 等 Android APP 配合,以及更方便地進行控制,我們可以將從串口讀取數據的方式改為透過網路,使用 TCP 協議來讀取數據。由於 ESP32 模組本身就具有 WiFi 功能,因此無需額外安裝硬體即可實現。
因為需要呼叫 WiFi 模組,因此我們需要在程式碼的開頭導入 WiFi 函式庫,以便後續呼叫。
包含 <WiFi.h>
接下來需要設定 WiFi 網路接入點的資訊,請在程式碼的開頭處進行設定,不要放在函數內。
const char* ssid = "您的 WiFi 名稱";
const char* password = "您的 WiFi 密碼"
對於 TCP 協議,需要定義一個端口來進行通訊。我們使用 80 端口,並在程式碼的標頭中設定:
const int serverPort = 80;
接下來將 TCP 模組設定為伺服器模式,並在程式碼的開頭進行配置:
WiFiServer 伺服器 (伺服器埠);
WiFiClient 類別名為「client」。
接下來需要在 `setup()` 函數中初始化 WiFi 並進行連接操作:
開始使用 WiFi (SSID 為 ssid,密碼為 password)。
當 (WiFi.status() 不等於 WL\_CONNECTED) 時,{
delay(1000);
將連接到 Wi-Fi...
}
Serial.println("已連接到WiFi");
`Serial.print("IP 位址:");`
使用 `Serial.println(WiFi.localIP())` 輸出 Wi-Fi 連線的本地 IP 位址。
其中 使用 `Serial.println(WiFi.localIP())` 輸出 Wi-Fi 連線的本地 IP 位址。這段程式碼會將 ESP32 開發板的 IP 位址輸出到序列埠,您也可以在路由器的後台取得。
接下來仍然是在 `setup()` 函數中,啟動 TCP 伺服器:
伺服器開始。
Serial.println("TCP伺服器已啟動");
因為我們已經改用 WiFi 來讀取資料,而不是使用串行埠,因此需要將原本的…讀取序列資料函數已更換為讀取 Wi-Fi 資料函數,函數內的程式碼如下,但以下內容會詳細說明:
如果沒有活躍的客戶端
client = server.available(); // 檢查新的連線
如果 `client` 不為空
Serial.println("已連接新的客戶");
}
} else { // 處理已連接的客戶端
如果 (client.connected()) && client.available() >= 7) {
int rlen = client.readBytes(buf, 7);
如果 (rlen 等於 7) && buf\[0] 等於 0xFF && buf[1] 等於 0x01 && buf[3] 不等於 0x53 && 如果 buf[3] 不等於 0x51) {
整數值 `command` 取決於 `buf[3]` 的內容。
如果 (command 不等於 currentCommand) {
執行Pelco D 命令 (command);
currentCommand = command;
}
這樣就能成功從網路讀取 Pelco-D 命令。
後記
感謝大家的閱讀,本文是在我的個人部落格在 GitHub 上發布,並傳送到各大論壇。如有任何問題,請及時聯繫。由於筆者的技術能力有限,若有錯誤,敬請諒解。如果您喜歡這個專案,請按讚 (Star)。如有疑問,請前往 GitHub 建立 issue。