创意方案介绍
在这个项目中我制作一款步进电机FOC驱动,可以三闭环控制标准42步进电机,体积小,可以直接安装在步进电机尾部。
作为工业4.0方向应用,该电机驱动支持多种通信方式,比如CAN,MODBUS,RS485,UART以及常见的EN、DIR、STP控制,还要有工业上要求的各种毛刺保护电路。
同时,作为物联网方向应用,该电机驱动还拥有无线控制能力,支持MQTT协议,可使用MQTT对电机进行完全控制。
方案框图介绍
本项目使用的核心模块有两个,其中主控使用ESP32-S3,电机驱动使用的是DRV8962。为了可以自由调整DRV8962上的限流斩波控制阈值,还需要一块MCP4725来输出模拟量信号,控制DRV8962上的Vref。
DRV8962的集成度非常高,上面已经集成了低边电流测量功能,板载再加一个编码器识别电机角度即可。我选择的是AS5047P。
其他的模块是为了一些通信和调试功能服务。
方案中用到的指定厂商元器件介绍
1,ESP32-S3 是一款低功耗的MCU 系统级芯片(SoC),支持2.4 GHz Wi-Fi 和低功耗蓝牙(Bluetooth® LE) 无线通 信。 芯片集成了高性能的Xtensa® 32 位LX7 双核处理器、超低功耗协处理器、Wi-Fi 基带、蓝牙基带、RF 模块 以及外设。
2,MCP4725是一个低功耗,高精度,单通道,12位缓冲电压输出数字到模拟转换器(DAC)与非易失性存储器(EEPROM)。 它的板载精度输出放大器允许它实现轨到轨模拟输出摆动。 DAC输入和配置数据可以被编程到非易失性存储器(EEPROM)由用户使用I2C接口命令。
原理图介绍
先看下原理图全貌
其中,ESP32-S3单片机部分,除了boot和en引脚的按钮以外,还增加了串口以及USB-CDC两个USB接口,方便使用各种不同的方式进行开发调试,增加电路板的可玩性。
由于步进电机的电压一般都高于12V,降压至3.3V给单片机使用时,压降过大,因此需要使用一款电压范围较大的DC-DC芯片来完成电压转换。我选的是LM5164.
位置传感器我用的是AS5047P,这款磁编码器在使用SPI通信时可以做到绝对量的输出,而且分辨率高达14位。
通信芯片除了GPIO直接引出外,我还增加了RS485和CAN通信芯片。选型时需要注意一定要选用支持3.3V的版本。终端电阻已经设计在板上,可以通过跳线帽选择是否接入。
最核心的部分是这一颗TI的双全桥电机驱动,不但支持很宽的电压范围,而且里面还自带了电流放大器以及低边电流采样电阻,相当于无需任何外部元件就可以实现内部的斩波限流。因为我们要使用FOC来控制电机,自然需要两相的电流检测。由于电流测量值是通过电流的方式来输出的,因此需要一个接地电阻来把电流输出值转化为电压输出值。
电源输入部分我还增加了两颗电解电容。大电解电容的目的不光光是作为一个瞬间大功率的备用电流源使用,更重要的是电解电容具备一定的寄生电阻。如果缺乏这个电阻缓冲,当突然上电的时候,瞬间给陶瓷电容充电,电路中的寄生电感可能会在输入端产生一个非常大的电压毛刺,这个毛刺会烧穿后面的电路。而电解电容的寄生电阻在这里就能充当一个很好的阻尼器。
恒流斩波是通过Vref引脚来进行设置的。一般情况我们会在板子上放一个电位器来设置。但这个模块我设计初衷是具备物联网控制功能的,所以最好所有的设置都可以通过软件完成,不需要实际接触到电路板。因此我又用了一个I2C串口的DAC来给Vref提供参考电压,设置斩波阈值。
引出的所有外部引脚都增加了TVS二极管,一般工业应用上都会有这一类的保护要求,用来对高压和脉冲静电进行防护。
PCB设计介绍
最常用的步进电机是42电机,因此这块板子的尺寸需要和42电机一致。而ESP32-S3模块比较大,因此我只好把通讯芯片和电源芯片这些高度不太高的芯片放在背面。焊接时两面都有元件。
3D图可以直观看出,四边中其中一个边用作大电流部分,电源输入以及给电机供电,使用螺丝端子进行连接;另一边是各种通讯信号线,也是用螺丝端子连接;第三条边放了两个USB口,用来做调试使用,最后一个边留给ESP32-S3模组的天线。
可以注意到大电流的区域我打了密密麻麻的过孔。这里的布线也非常讲究,不但要短,要大线宽,增加过流能力;更重要的是要尽可能减小环路面积,减少EMI。
焊接流程及成品展示
制作回来的的电路板非常漂亮
由于正面的元件很多,焊接时可以先把正面的元件用锡膏和铁板烧焊接好,然后再去手工焊接背面的元件。
正面的两个跳线帽是用来接入终端匹配电阻使用的。如果不需要可以像我这样只插一个针,避免需要时找不到跳线帽。
代码介绍
基础代码使用simpleFOC进行构建,开发环境我直接用的是Arduino IDE。
电机配置部分并不复杂,直接使用官方的example就可以,配置好驱动,位置传感器和电流传感器,再把它们连接到电机就行。电机部分的基本参数配置可以先在setup()函数中写好:
// motor config
motor.voltage_sensor_align = driver.voltage_power_supply * 0.8;
motor.controller = MotionControlType::angle;
motor.PID_velocity.P = 0.2;
motor.PID_velocity.I = 20;
motor.PID_velocity.D = 0.001;
motor.PID_velocity.output_ramp = 1000;
motor.LPF_velocity.Tf = 0.01;
motor.P_angle.P = 20;
motor.P_angle.I = 0;
motor.P_angle.D = 0;
motor.P_angle.output_ramp = 10000;
motor.LPF_angle.Tf = 0;
motor.motion_downsample = 10;
这其中第一行代码非常非常关键。arduinoFOC的官方比较保守,在默认配置的yaml文件中将电机在初始化时的电压(也就是PWM)设置为0.3,避免烧坏电机。但我在实际测试时发现,当使用12V驱动电机时,0.3的默认值根本无法让电机转动,这就会导致初始化频频失败。坑爹的是,不在调试模式下,根本不知道初始化出了问题,只会发现每次上电后电机的表现都不一样(因为初始化失败,simpleFOC估计的参数可以是任何值);而即便打开了debug功能,也只能看到初始化失败,但并不知道为什么。我在这里耗费了大量的时间,希望其他人不再踩坑。
另外一个就是我在最后一行增加的motor.motion_downsample设定。这个主要是为了磁编码器的速度计算来设置的。由于磁编码器的采样率很高,每次采样间隔时间电机转过的角度太小,这就导致计算出的速度波动性过大,基本无法使用。这个问题是我在直接读取传感器数据时发现的, 这里手动把他设置为100,可以让电机很平稳的工作。
代码中最关键的部分是控制模式输入输出部分。simplefoc有一套commander指令,可以对电机参数和指令进行非常完全的控制。但这套指令只能通过Serial Stream buffer使用。而现在我们想让他可以通过RS485, CAN甚至是MQTT控制,不太优雅的方式是直接写个简单的命令,在loop中轮询,或是通过回调函数,来直接set target。但我们当然会使用更优雅的方式,就是直接把整套commander接口暴露到其他的通信端口上。
首先先说传统的DIR, STEP控制。这个在simpleFOC里可以直接实现,比较简单。
StepDirListener step_dir = StepDirListener(2, 42, _2PI / 200.0);
void onStep() {
step_dir.handle();
}
void setup() {
// init step and dir pins
step_dir.init();
step_dir.enableInterrupt(onStep);
step_dir.attach(&motor.target);
}
RS485也是串口通讯的一种,只是在电平上采用了差分方式,因此两根线只能半双工通讯。所以这个也比较好实现,我们只要把Serial1配置为半双工模式,并且使用接在485芯片上的引脚就可以实现。
// instantiate the RS485 interface
#define RS485_RX_PIN 18
#define RS485_TX_PIN 17
#define RS485_RTS_PIN 38
#define RS485_BAUD 9600
#define RS485 Serial1
Commander commander_rs485 = Commander(RS485);
void onMotor_rs485(char* cmd) {
commander_rs485.motor(&motor, cmd);
}
void setup() {
// init RS485
RS485.begin(RS485_BAUD, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN);
while (!RS485) {
delay(10);
}
if (!RS485.setPins(-1, -1, -1, RS485_RTS_PIN)) {
Serial.print("Failed to set RS485 pins");
}
if (!RS485.setMode(UART_MODE_RS485_HALF_DUPLEX)) {
Serial.print("Failed to set RS485 mode");
}
commander_rs485.add('M', onMotor_rs485, "full motor config");
}
而CAN就麻烦的多,这里我们新建一个头文件,基于arduino原本的Stream类创建一个新的子类,然后通过重写原本类中的方法来实现功能。在我们的主程序中就可以把这个子类作为参数传递给Commander来进行使用。由于CAN一次只能传送8个字节的数据,指令都在8字节以内,但指令运行成功的回报信息都在8字节以上,因此我对回报打印的函数进行了一些修改,只要发现有回报内容,说明指令执行成功,那么CAN只需要返回一个1就可以代表指令执行成功。
#ifndef CanSerial_h
#define CanSerial_h
#include <ESP32-TWAI-CAN.hpp>
class CanSerial : public Stream {
public:
CanSerial() {}
~CanSerial() {}
bool begin(uint8_t CAN_RX_PIN, uint8_t CAN_TX_PIN, uint16_t CAN_SPEED, uint8_t CAN_ID) {
_ID = CAN_ID;
txFrame.identifier = _ID;
txFrame.extd = 0;
txFrame.data_length_code = 1;
txFrame.data[0] = 1;
return ESP32Can.begin(ESP32Can.convertSpeed(CAN_SPEED), CAN_TX_PIN, CAN_RX_PIN, 10, 10);
}
int available() override {
if (_available) { return 1; }
if (ESP32Can.readFrame(rxFrame, 0)) {
if (rxFrame.identifier == _ID) {
_available = 1;
return rxFrame.data_length_code;
}
}
return 0;
}
int read() override {
if (_available) {
if (_pos < rxFrame.data_length_code) {
return rxFrame.data[_pos++];
} else {
_pos = 0;
_available = 0;
return '\n';
}
} else {
return -1;
}
}
int peek() override {
if (_available) {
return rxFrame.data[_pos];
} else {
return -1;
}
}
size_t write(uint8_t byte) override {
if (byte != '\r' && byte != '\n') {
_buffer_tx[_pos++] = byte;
} else if (byte == '\n') {
ESP32Can.writeFrame(txFrame);
Serial.print("CAN:");
Serial.println(_buffer_tx);
memset(_buffer_tx, 0, sizeof(_buffer_tx));
_pos = 0;
}
return 1;
}
private:
CanFrame rxFrame;
CanFrame txFrame;
uint8_t _ID;
uint8_t _pos = 0;
bool _available = 0;
char _buffer_tx[64] = { 0 };
};
#endif
最后MQTT的实现方式与CAN类似。唯一的区别是MQTT不再有字节数限制,因此我们可以把返回的数据原原本本的通过MQTT Publish出去。
#ifndef MQTTSerial_h
#define MQTTSerial_h
#include <WiFi.h>
#include <PubSubClient.h>
class MQTTSerial : public Stream {
public:
MQTTSerial()
: _MQTTclient(_wifiClient) {}
~MQTTSerial() {}
void loop() {
reconnect();
_MQTTclient.loop();
}
void begin(const char* ssid,
const char* password,
const char* mqtt_server,
const int port,
const char* MQTT_usr,
const char* MQTT_pwd,
const char* client_ID,
const char* topic_rx,
const char* topic_tx) {
_MQTT_usr = MQTT_usr;
_MQTT_pwd = MQTT_pwd;
_client_ID = client_ID;
_topic_rx = topic_rx;
_topic_tx = topic_tx;
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {}
_MQTTclient.setServer(mqtt_server, port);
_MQTTclient.setCallback([this](char* topic, byte* payload, unsigned int length) {
this->callback(topic, payload, length);
});
}
int available() override {
return _available;
}
int read() override {
if (_available) {
if (_pos < _length) {
return _buffer_rx[_pos++];
} else {
_pos = 0;
_available = 0;
return '\n';
}
} else {
return -1;
}
}
int peek() override {
if (_available) {
return _buffer_rx[_pos];
} else {
return -1;
}
}
size_t write(uint8_t byte) override {
if (byte != '\r' && byte != '\n') {
_buffer_tx[_pos++] = byte;
} else if (byte == '\n') {
_MQTTclient.publish(_topic_tx, _buffer_tx);
Serial.print("MQTT:");
Serial.println(_buffer_tx);
memset(_buffer_tx, 0, sizeof(_buffer_tx));
_pos = 0;
}
return 1;
}
private:
WiFiClient _wifiClient;
PubSubClient _MQTTclient;
const char* _MQTT_usr;
const char* _MQTT_pwd;
const char* _client_ID;
const char* _topic_rx;
const char* _topic_tx;
uint8_t _pos = 0;
bool _available = 0;
unsigned char* _buffer_rx;
char _buffer_tx[64] = { 0 };
uint16_t _length;
void reconnect() {
if (!_MQTTclient.connected()) {
if (_MQTTclient.connect(_client_ID, _MQTT_usr, _MQTT_pwd)) {
_MQTTclient.subscribe(_topic_rx);
}
}
}
void callback(char* topic, byte* message, unsigned int length) {
_available = 1;
_buffer_rx = message;
_length = length;
// Serial.print("Message arrived on topic: ");
// Serial.print(topic);
// Serial.print(". Message: ");
// for (int i = 0; i < _length; i++) {
// Serial.print((char)_buffer_rx[i]);
// }
// Serial.println();
}
};
#endif
功能展示
42电机,最好直接使用这种电机尾轴上带有径向磁铁的。如果没有,自己买径向磁铁,然后粘到后轴上面也可以。
磁编码器正好放在磁铁上方,连接好电机和驱动器,就完成了全部装配。电机和驱动器之间需要放一个合适长度的隔离柱,确保引脚不要接触到电机,但同时也不能让磁编码器距离径向磁铁过远。
RS485通讯测试,可以使用一块arduino单片机与485模块制作一个调试器来完成。调试器的代码我会包含在项目文件中。
CAN通讯测试,我使用的是稚晖君的PicoDK开发板来充当调试器使用,因为这颗开发板上自带了CAN通信芯片。调试器的代码我会包含在项目文件中。
具体的功能演示,由于需要观察电机的动态情况,图片难以展示,大家可以直接参考视频中的内容。
心得体会
我一直对自己DIY一台工业机械臂有兴趣,而最近又突然有需求需要制作一个小巧便携的智能家居窗帘电机。这次活动一次性满足我的这两个需求。经过这次活动的制作,不但熟练了硬件设计,选型与焊接技能,更让我第一次仔细去研究arduino中的方法,通过重写原生库来实现功能拓展,收获非常大。希望像Fastbond这样灵活的创意活动越办越好!