封面摄于2025春节在台州时
本文约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库,以便后续调用:
#include <Arduino.h>
ESP32是单片机,依靠循环来完成相应代码任务,即为循环loop()函数中的代码。基本思路是不断循环读取串口的数据,若读到相应命令,则进行转向控制后继续读取串口数据,若串口发来停止转动指令,或新的指令,则执行新的指令。
首先,让我们在setup()函数中对单片机启动时进行初始化。
配置波特率为9600串口:
Serial.begin(9600);
初始化板载LED指示灯以及对于控制步进电机控制板的相关IO引脚:
// 设置IO输入输出
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);
// 初始化信号灯
digitalWrite(LED_MSG_PIN, LOW);
初始化用于控制云台转速的硬件级PWM输出端口:
const int PWM_FREQ = 10000; // PWM 频率(单位:Hz)
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);
ledcAttachPin(EL_SPEED_PUL_PIN, 1);
数据的读取:
在setup()初始化完一切需要用到的东西后,我们就可以进行对串口发来的Pelco-D命令进行接收。
我们定义一个新的函数,名为readSerialData()
,用于读取串口数据。
参阅Pelco-D的开发手册,注意到标准Pelco-D消息由7个字节的16进制数据组成,其中要用到的是两个数据位以及最后一个校验位。
我们先用代码读取7个字节的数据:
int rlen = Serial.readBytes(buf, 7);
然后利用校验位校验数据是否正确,若正确再做下一步拆解处理:
if (rlen == 7 && buf[0] == 0xFF && buf[1] == 0x01)
为了同一个数据被进行多次解析执行,还要添加判断数据是否于上一次读取到的不同,若不同,再执行。如果这个判读不做会出现转动一卡一卡的问题:
if (command != currentCommand) // 只有在接收到新的命令时才处理
{
handlePelcoDCommand(command);
currentCommand = command; // 更新当前命令
}
至此数据的读取与拆解完毕。
数据的执行:
我们定义一个函数handlePelcoDCommand(int command)
用于解析读取的数据并进行对步进电机驱动板的控制。
其中传入的int类型command变量是上文中分解到的最终控制命令,Pelco-D协议的基本转向命令分为停止、上、下、左、右,分别对应数据0、2、4、8、16,当然还有左上、右下之类的,这里不过多赘述。
我们先创建一个索引(其实官方名称我不确定是不是这个,叫习惯了)switch (command)
,将所有命令放入这个索引中。
接下来我们以右转为例,首先右转对应的命令位2,我们在索引中写入:
case 2:
这代表当传入的command变量为2时执行接下来的代码块。
然后我们将控制水平的Az驱动电机的引脚设置为高电平:
digitalWrite(AZ_DIRECTION_PIN, HIGH);
并设置PWM调速命令执行的bool值为True:
is_azcontrol_stepper = true;
只有这样电机才会知道要按照什么速度转动,以上二者缺一不可。
这个bool变量在下一章的PWM调速中会提及。
于是控制右转的完整代码块为:
case 2: // 右转
digitalWrite(AZ_DIRECTION_PIN, HIGH);
is_azcontrol_stepper = true;
Serial.println("0002.");
其中 Serial.println("0002.");
为向串口输出调试数据,用于程序的调试,可忽略不写。
loop()函数以及PWM调速:
我们在上文提及,ESP32系列芯片是在循环某个代码块以达到相应目的。
我们想让上面的函数执行起来,必须将他们加入至loop()函数中**(setup()函数除外)**,否则他们不会被执行。
我们在主程序中创建loop()函数:
void loop()
{
....这里放你的代码或函数....
}
首先将我们上文写到的读取串口数据函数丢进去:
readSerialData();
然后要判断上文的PWM调速的bool变量是否为True,如果为True,则启动PWM调速(这里可以另开一个函数,我懒就直接丢loop()里了):
if (is_azcontrol_stepper)
{
ledcWrite(0, 128);
}
if (is_azcontrol_stepper == false)
{
ledcWrite(0, 0);
}
垂直方向的El电机同理。
解释一下ledcWrite(int a, int b);
这条调用。其中int a代表了PWM输出通道,在前文setup()函数中我们将0通道指向了25号IO引脚,所以他就会在25号引脚输出方波PWM信号;int b代表了输出信号的频率,通过调整这个变量可以调控相应电机的转动速度,0则为停止。
最后在loop()里放入通过串口发送当前运行状态的函数:
printStatus();
这个函数下一章会详细阐述。
为了让这个函数不阻塞串口以及程序,我们对他加上一个定时器,定时执行:
if (currentMillis - previousMillis >= interval)
{
previousMillis = currentMillis;
printStatus();
}
状态回传函数:
为了在电脑端可以顺利了解云台运动状态,我们可以通过串口回传云台的相关状态。
我们定义一个printStatus();
函数,用于存放状态回传的代码块。
其中代码就是将目前状态通过串口打印,不过多赘述:
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");
具体包含了当前云台是否转动以及转向方向。
更进一步:WIFI控制
为了适配DTrac等安卓APP,也为了更方便的控制,我们可以将从串口读取数据改为从网络通过TCP协议读取数据。由于ESP32模块自带WiFi,故不需要加装其他硬件便可实现。
因为要调用WiFi模块,我们需要在代码开头导入WIFI库,以便后续调用:
#include <WiFi.h>
接下来要配置WiFi接入点信息,在代码头部配置,不要放入函数中:
const char* ssid = "你的WiFi名";
const char* password = "你的WiFi密码";
对于TCP协议,需要定义一个端口来通讯。我们使用80端口,在代码头部配置:
const int serverPort = 80;
接下来设置TCP模块为服务器模式,在代码头部配置:
WiFiServer server(serverPort);
WiFiClient client;
接下来要在setup()函数中初始化WiFi并进行连接操作:
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());
其中 Serial.println(WiFi.localIP());
语句会在串口输出ESP32开发板的IP地址,也可以前往路由器后台获取。
接下来还是在setup()函数中,启动TCP服务器:
server.begin();
Serial.println("TCP Server started");
由于我们已经不用串口读取数据,而是改为WiFi读取,所以要将原来的readSerialData()
函数改为readWifiData()
函数,函数内代码如下,不过多赘述:
if (!client) { // 如果没有活跃的客户端
client = server.available(); // 检查新连接
if (client) {
Serial.println("New client connected");
}
} else { // 处理已连接的客户端
if (client.connected() && client.available() >= 7) {
int rlen = client.readBytes(buf, 7);
if (rlen == 7 && buf[0] == 0xFF && buf[1] == 0x01 && buf[3] != 0x53 && buf[3] != 0x51) {
int command = buf[3];
if (command != currentCommand) {
handlePelcoDCommand(command);
currentCommand = command;
}
这样就能成功从网络读取到Pelco-D命令。
后记
感谢各位的阅读,本文在我的个人博客中发布,并搬运至各大论坛。若有不妥之处,请及时联系。笔者技术力有限,若有错误,敬请谅解。若喜欢这个项目,麻烦Star~若有疑问,请前往Github创建issue。
最后,用一张可爱的CG来收尾吧~
(图片版权信息:来自《冬日树下的回忆》,MagicaLuv制作组版权所有。本人已购买其CG包,合法使用于本文,用途为装饰。)