This article is approximately 2600 words long and took 120 minutes to write.
Special thanks to:BI6OPR The proposed approach and the original program.
This article followsCC BY-NC-SA 4.0Agreement, retaining copyright.
Let's begin!
Introduction
I am in the process of building my own radio astronomy telescope, and the parabolic dish is ready. However, I was shocked by how expensive the rotation mechanism turned out to be. For example, a Yaesu G5500 costs several thousand dollars, which is simply unaffordable. Thanks to some advice from experienced individuals in the community, I discovered a heavily modified stepper motor mount that had been disassembled. I learned that BI6OPR, a respected member of the community, had already made preliminary adaptations for the Pelco-D protocol, but it was still not fully functional. With my limited programming knowledge, and following the guidance of BI6OPR and online resources, I finally managed to create a relatively satisfactory Pelco-D decoder based on the original program provided by BI6OPR. This article is intended to share some of my own experiences and insights, with the hope that it will provide helpful ideas for others.
Regarding hardware
AuthorConsidering my own technical capabilities.Ultimately, we abandoned the STM32 and chose the ESP32 series of chips, which are more suitable for IoT applications. The basic idea is to use the IO pins of the ESP32 chip to control the pan's direction and adjust its speed using PWM. We then establish communication with a computer using the ESP32 chip's TTL serial port, receiving steering commands, and sending steering status information to the computer. The stepper motor driver uses a 24V AC high-power drive board.
Note: This article primarily shares the firmware code portion; for information about the hardware, please refer to the GitHub repository.
Code implementation
Due to the use of ESP32 series chips, C++ was chosen for development in both MicroPython and C++.(This isn't because it was abandoned due to incompatibility with the new version of PyCharm.)The IDE uses our beloved VS Code, combined with PlatformIO, making development quite comfortable.
We are now introducing the Arduino library at the beginning of our code to facilitate later use.
#include <Arduino.h>
The ESP32 is a microcontroller that uses loops to execute specific code tasks, which are the codes within the `loop()` function. The basic idea is to continuously loop and read data from the serial port. If a specific command is detected, it performs steering control and continues reading data from the serial port. If the serial port sends a command to stop rotating or a new command, that command is executed.
First, let's initialize the microcontroller at startup within the setup() function.
Configure baud rate to 9600 for serial port:
Serial.begin(9600);
Initialize the onboard LED indicators and the relevant I/O pins on the stepper motor control board.
// Configure I/O input and output
const int AZ_DIRECTION_PIN = 26;
const int EL_DIRECTION_PIN = 27;
pinMode(LED_MSG_PIN, OUTPUT);
pinMode(AZ_DIRECTION_PIN, OUTPUT);
pinMode(EL_DIRECTION_PIN, OUTPUT);
pinMode(AZ_SPEED_PUL_PIN, OUTPUT);
pinMode(EL_SPEED_PUL_PIN, OUTPUT);
// Initialize traffic lights
digitalWrite(LED_MSG_PIN, LOW);
Initialize hardware-level PWM output ports for controlling pan/tilt speed:
const int PWM\_FREQ = 10000; // PWM frequency (units: Hz)
const int PWM\_RESOLUTION = 8; // Resolution (with 8 bits, the duty cycle range is 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);
ledcAttachPin(EL_SPEED_PUL_PIN, 1);
Data Reading:
Once the `setup()` function has initialized all necessary components, we can begin receiving Pelco-D commands sent over the serial port.
We define a new function calledreadSerialData(), used for reading serial port data.
Refer to the Pelco-D development manual, noting that standard Pelco-D messages consist of 7 bytes of hexadecimal data, using two data bits and the last check bit.
First, we use code to read 7 bytes of data:
int rlen = Serial.readBytes(buf, 7);
Then, use a checksum to verify the integrity of the data. If the data is correct, proceed with the next step of unpacking it.
if (rlen == 7) && buf[0] == 0xFF && if buf[1] == 0x01
To ensure that the same data is parsed and executed multiple times, it's necessary to check if the data differs from the last time it was read. If there's a difference, the process should be repeated. Failing to do so could lead to issues where each card is processed independently.
if (command != currentCommand) { // Only process when a new command is received
{
handlePelcoDCommand(command);
currentCommand = command; // Update the current command
}
The reading and parsing of the data are now complete.
Executing the data:
We define a function.handlePelcoDCommand(int command)Used for parsing and controlling the stepper motor driver board based on the data read.
The incoming `int` type command variable represents the final control command that was broken down in the previous context. The basic turning commands in the Pelco-D protocol are stop, up, down, left, and right, which correspond to data values of 0, 2, 4, 8, and 16, respectively. There are also commands like "upleft" and "downright," but I won't elaborate further here.
First, we create an index (the official name for this might be different).I'm used to calling it that.)toggle (command)Place all commands within this index.
Next, let's take a right turn as an example. First, we set the corresponding command bit to 2 for a right turn. Then, in the index, we write:
Case 2:
This means that the next code block will be executed when the incoming `command` variable is equal to 2.
Then we will set the pins of the Az-controlled horizontal motor to a high level.
digitalWrite(AZ_DIRECTION_PIN, HIGH);
And set the boolean value for the execution of the PWM speed control command to True:
is_azcontrol_stepper = true;
Only then will the motor know what speed to rotate at. Both of these are essential.
This boolean variable will be mentioned in the next chapter, which discusses PWM speed control.
Therefore, the complete code block for controlling the right turn is:
case 2: // Turn right
digitalWrite(AZ_DIRECTION_PIN, HIGH);
is_azcontrol_stepper = true;
Serial.println("0002.");
Among them Serial.println("0002.");To output debugging data to the serial port for program debugging, it can be ignored and not written.
The `loop()` function and PWM speed control:
As we mentioned earlier, the ESP32 series of chips operate by repeatedly executing a specific code block to achieve a particular objective.
We want to make those functions execute, so we need to include them within the `loop()` function.(excluding the setup() function)Otherwise, they will not be executed.
We create a loop() function within the main program:
void loop()
{
...Place your code or function here...
}
First, let's incorporate the serial data reading function we described earlier:
readSerialData()
Then, you need to check if the boolean variable representing the PWM speed control is True. If it's True, start the PWM speed control (you can also create a separate function for this).I just dropped it directly into the loop().):
if (is_azcontrol_stepper)
{
ledcWrite(0, 128);
}
if (is_azcontrol_stepper == false)
{
ledcWrite(0, 0);
}
Similarly, the El motor in the vertical direction.
Explain the function ledcWrite(int a, int b);This call. Where `int a` represents the PWM output channel, and in the previous `setup()` function, we mapped channel 0 to pin 25. Therefore, it will output a square wave PWM signal on pin 25; `int b` represents the frequency of the output signal, and by adjusting this variable, you can control the speed of the corresponding motor. When set to 0, the motor stops.
Finally, place the function that sends the current running status via serial port within the loop().
printStatus();
This function will be described in detail in the next chapter.
To prevent this function from blocking the serial port and the program, we added a timer to it, which executes periodically:
if (currentMillis - previousMillis) >= interval)
{
previousMillis = currentMillis;
printStatus();
}
Status feedback function:
To enable smooth monitoring of pan and tilt movement status on a computer, we can transmit relevant pan and tilt status information via serial communication.
We define aprintStatus();Function, used to store code blocks for returning state information.
One of the methods involves printing the current status through the serial port. I will not elaborate further on this.
Serial.print("Current Status - AZ Direction: ");
Serial.print(az_stepper_direction ? "Forward" : "Backward");
Serial.print(", EL Direction: ");
Serial.print(el_stepper_direction ? "Forward" : "Backward");
Serial.print(", AZ Control: ");
Serial.print(is_azcontrol_stepper ? "On" : "Off");
Serial.print(", EL Control: ");
Serial.println(is_elcontrol_stepper ? "On" : "Off");
Specifically, this includes whether the pan/tilt mechanism is currently moving and in which direction.
Furthermore: Wi-Fi control
To adapt to apps like DTrac on Android, and for easier control, we can switch from reading data via serial port to reading data through the network using TCP protocol. Since the ESP32 module has built-in WiFi, no additional hardware is required to achieve this.
Since we need to use the WiFi module, we need to import the WIFI library at the beginning of the code so that it can be called later.
#include <WiFi.h>
Next, you need to configure the Wi-Fi access point information. Configure this in the header of the code, not within a function.
const char* ssid = "Your WiFi name";
const char* password = "Your WiFi password";
For TCP protocol, a port needs to be defined for communication. We use port 80, and configure it in the code header:
const int serverPort = 80;
Next, configure the TCP module to operate in server mode, and add the following configuration at the top of the code:
WiFiServer server(port);
WiFiClient client;
Next, you need to initialize WiFi and perform the connection operation within the `setup()` function.
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
serial.print("IP address: ")
Serial.println(WiFi.localIP());
Among them Serial.println(WiFi.localIP());The command will output the IP address of the ESP32 development board to the serial port, or you can obtain it from the router's administration interface.
Next, within the `setup()` function, start the TCP server:
start server;
Serial.println("The TCP server has been started.");
Since we are now using Wi-Fi to read data instead of serial ports, we need to replace the originalreadSerialData()The function has been modified to:readWifiData()Function, the code within the function is as follows (but with minimal elaboration):
if (!client) { // If there is no active client
client = server.available(); // Check for new connection
if (client) {
Serial.println("A new client has connected.");
}
} else { // Handle connected clients
if (client.isConnected()) && client.available() >= 7) {
int rlen = client.readBytes(buf, 7);
if (rlen == 7) && buf[0] == 0xFF && buf[1] == 0x01 && buf[3] is not equal to 0x53 && if (buf[3] != 0x51) {
int command = buf[3];
if (command is not equal to currentCommand) {
handlePelcoDCommand(command);
currentCommand = command;
}
This allows for successful retrieval of Pelco-D commands from the network.
Epilogue
Thank you for reading this article. In myPersonal blogThis project is released on GitHub and will be shared on major forums. If you find any issues, please contact us promptly. As a technical writer with limited expertise, I apologize for any errors. If you like this project, please star it.If you have any questions, please create an issue on GitHub.