DC Motor集成MEGA328和L293DD芯片,拥有4个电机驱动通道,采用直流电源输入设计用于功率补充,并通过M-BUS,自动为顶部的M5Core供电。 使用DC Motor模块能够简单快速的驱动RJ12接口编码电机,比如乐高ev3电机。
产品详细介绍:https://docs.m5stack.com/zh_CN/module/lego_plus
DC Motor与M5Core采用I2C方式通信,我们从官方提供的示例程序来看看M5Core是如何与DC Motor交互的。
1.Setup初始化
void setup() {
M5.begin(); //启动M5
M5.Power.begin(); //电源
Wire.begin(); //启动Wire
Serial.begin(115200); //启动串口,用于调试
// 其它为设置LCD显示的代码,本文不做说明。
}
2.Loop主循环
void loop() {
motor_demo();
}
主循环里调用了motor_demo方法,此方法通过M5Core上三个按键来与DC Motor交互,A按键作用为减速,B按钮作用为停止,C按键作用为加速。
3.motor_demo方法
void motor_demo(void){
uint8_t BtnFlag = 0;
M5.update();
// 监听三个按键的释放事件(wasReleased),如有按键被释放(即按键按下后放开)
// 则对速度变量Speed做相应赋值处理,并设置标记变量BtnFlag为1
if (M5.BtnA.wasReleased()) {
Speed -= STEP_V;
if(Speed <= -255)
Speed = -255;
BtnFlag = 1;
}else if (M5.BtnB.wasReleased()) {
Speed = 0;
BtnFlag = 1;
}
else if (M5.BtnC.wasReleased()) {
Speed += STEP_V;
if(Speed >=255)
Speed = 255;
BtnFlag = 1;
}
//当BtnFlag为1时,调用MotorRun方法以指定速度运行电机。
if(BtnFlag == 1){
BtnFlag = 0;
for(int i =0;i<4;i++){
MotorRun(i,Speed);
M5.Lcd.setCursor(X_LOCAL, Y_LOCAL + YF*i , FRONT);
M5.Lcd.printf("S%d:%d \n",i,Speed);
}
}
//不论是否有按键操作,总是调用ReadEncoder方法读取电机的编码(旋转)数据显示在LCD屏上。
for(int i=0;i<4;i++){
M5.Lcd.setCursor(X_LOCAL + XF*4, Y_LOCAL + YF*i , FRONT);
M5.Lcd.printf("E%d:%d \n",i,ReadEncoder(i));
}
}
本方法还可以做适当优化,即如果在按键释放的处理代码中,Speed的值相对于前一值无变化,不用设置BtnFlag为1。
4.MotorRun方法
本方法通过I2C协议将速度数据写入DC Motor模块,DC Motor根据速度数据控制电机作相应动作。
int32_t MotorRun(uint8_t n,int16_t Speed){
if(n>3)
return 0;
if(Speed <= -255)
Speed = -255;
if(Speed >=255)
Speed = 255;
Util.writeBytes(SLAVE_ADDR,MOTOR_ADDR_BASE+n*2,(uint8_t *)&Speed,2);
return 1;
}
速度值为int16_t类型,即2个字节(-255 - 255)。
5.ReadEncoder方法
本方法通过I2C协议从DC Motor模块请求电机编码数据,DC Motor根据请求数据返回电机编码数据。
int32_t ReadEncoder(uint8_t n){
uint8_t dest[4]={0};
if(n>3)
return 0;
Util.readBytes(SLAVE_ADDR,ENCODER_ADDR_BASE+n*4,4,dest);
return *((int32_t*)dest);
}
编码数据值为int32_t类型,即4个字节。
关于I2C写入和读取数据的分析见第四部分。
从上面的分析可以得知,DC Motor作为I2C从设备,需要根据I2C接口的通信来处理是控制电机动作、还是返回电机编码数据等,而DC Motor控制了4个电机,控制就需要8个I/O端口(其中4个支持PWM输出,用于调速),获取编码数据也需要8个I/O端口(能配置中断),DC Motor采用了MEGA328和L293DD的组合(类似于Arduino和L298组合),使用两个L293来驱动4个电机,电路草图如下(省略另一路L293和供电部分)
官方原理图
1.常数,全局变量定义
/*** 系统常数 ***/
#define FIRWMARE_VER_ADDR 0x64 //固件版本读取地址
#define I2C_SET_ADDR 0x63 //I2C设置地址
/*** I2C从机 ***/
uint8_t i2c_slave_address = 0;
uint8_t i2c_read_address = 0;
uint8_t i2c_registers[32] = {0}; //32字节数据存储
#define SLAVE_ADDR 0x56 //I2C从机通讯地址
#define I2C_ADDR_OFFSET 0 //数据存储偏移地址
#define MOTOR_CTRL_ADDR = (I2C_ADDR_OFFSET + 0) //电机控制数据起始地址
#define MOTOR_CTRL_LEN = 2 // 电机控制数据长度
#define NUMS_OF_MOTOR = 4 // 电机数量
#define MOTOR_TOTAL_LEN = (MOTOR_CTRL_LEN * NUMS_OF_MOTOR) //电机控制数据的总长度
#define ENCODER_READ_ADDR = (I2_ADDR_OFFSET + MOTOR_TOTAL_LEN) //编码器数据起始地址
#define ENCODER_READ_LEN 4
#define NUMS_OF_ENCODER 4
#define ENCODER_TOTAL_LEN (ENCODER_READ_LEN * NUMS_OF_ENCODER) //编码器数据的总长度
数据存储区示意图
2.初始化
/*** 电机控制类初始化 ***/
L293DDH motor1(M1_PWM_PIN, M1_DIR_PIN); //指定调速、方向引脚
......
// 取电机控制数据起始位置的地址,并转为int16_t类型指针
int16_t* motor_val = (int16_t*)(&(i2c_registers[MOTOR_CTRL_ADDR]));
/*** 编码器类初始化 ***/
Encoder encoder1(M1_ENC_A_PIN, M1_ENC_B_PIN); //指定编码数据输入引脚,需支持中断
......
// 取编码数据起始位置的地址,并转为int32_t类型指针
int32_t* encoder_val = (int32_t*)(&(i2c_registers[ENCODER_READ_ADDR]));
/*** setup方法 ***/
void setup() {
Wire.begin(SLAVE_ADDR); // 做为从设备启动
Wire.onRequest(requestEvent); //设置响应主设备读数据的方法
Wire.onReceive(receiveEvent); //设置响应主设备写数据的方法
}
3.数据写入响应
读取主设备发来的数据
void receiveEvent(int howMany) {
uint8_t write_addr = Wire.read();
if (howMany == 1) {
// 只有一个字节,判定为定为电机控制数据位置地址(0-6)
i2c_read_address = write_addr;
} else if ((write_addr >= MOTOR_CTRL_ADDR) &&
(write_addr < ENCODER_READ_ADDR)) {
// 有多个字节,判定为电机控制数据,写入i2c_registers内
for (int i = 0; i < (howMany - 1); i++) {
((uint8_t *)motor_val)[write_addr - MOTOR_CTRL_ADDR + i] = Wire.read();
}
// 设置控制数据写入标志为TRUE,主循环根据此标志决定是否驱动电机
motor_write_flg = true;
}
}
由上面的代码可知,i2c_read_address默认值为0,主设备可以直接发送8字节的4个电机控制数据过来,
4.数据读取响应
向主设备发送数据
void requestEvent() {
if (i2c_read_address < MOTOR_CTRL_ADDR + MOTOR_TOTAL_LEN) {
// 数据读取位置在控制数据区域, 向主设备发送某个电机的控制数据(2字节)
Wire.write(i2c_registers[i2c_read_address]);
Wire.write(i2c_registers[i2c_read_address + 1]);
} else if (i2c_read_address < ENCODER_READ_ADDR + ENCODER_TOTAL_LEN) {
// 数据读取位置在编码器数据区域, 向主设备发送某个电机编码器数据(4字节)
for (int i = i2c_read_address; i < (i2c_read_address + 4); i++) {
Wire.write(i2c_registers[i]);
}
} else if (i2c_read_address == I2C_SET_ADDR) {
// 向主设备发送i2c_slave_address(未发现用途)
Wire.write(i2c_slave_address);
} else if (i2c_read_address == FIRWMARE_VER_ADDR) {
// 向主设备发送FIRWMARE_VER(版本号)
Wire.write(FIRWMARE_VER);
}
i2c_read_address = 0xff;
}
同样的,如果要读取指定电机的数据,主设备应先发送1个字节的数据位置地址过来。
5.主循环
更新编码器数据,驱动电机
void loop() {
// 读取编码器数据
encoder_val[0] = encoder[0]->read();
encoder_val[1] = encoder[1]->read();
encoder_val[2] = encoder[2]->read();
encoder_val[3] = encoder[3]->read();
// 根据电机控制数据写入标志判断是否要驱动电机。
if (motor_write_flg) {
motor_write_flg = false;
for (int i = 0; i < NUMS_OF_MOTOR; i++) {
motor[i]->set(-motor_val[i]);
}
}
}
6.电机驱动程序
void L293DDH::set(int value)
{
val = constrain(value, -255, 255);
if(value == 0)
{
// 停止
digitalWrite(dir_pin, 0);
analogWrite(pwm_pin, 0);
}
else if(value > 0 && value <= 255)
{
// 正向转动(相对),由PWM控制功率比例
digitalWrite(dir_pin, 0);
analogWrite(pwm_pin, value);
}
else if(value < 0 && value >= -255)
{
// 反向转动(相对),由PWM控制功率比例
digitalWrite(dir_pin, 1);
analogWrite(pwm_pin, 255 + value);
}
}
7.数据流图
关于编码器程序,老张会另写一文章来专门介绍。
四、I2C发送和读取数据
M5Stack库的CommUtil工具类对I2C数据读写做了封装,读写操作方法都有若干重载的版本,下面是其中两个方法。
1.发送数据
bool CommUtil::writeBytes(uint8_t address, uint8_t subAddress, uint8_t *data, uint8_t length) {
bool function_result = false;
Wire.beginTransmission(address); // 初始化
Wire.write(subAddress); // 1字节的数据位置(见上面的分析)
for(int i = 0; i < length; i++) {
Wire.write(*(data+i)); // 写数据
#ifdef I2C_DEBUG_TO_SERIAL
Serial.printf("%02x ", *(data+i));
#endif
}
function_result = (Wire.endTransmission() == 0); // 发送
return function_result;
}
2.读取数据
bool CommUtil::readBytes(uint8_t address, uint8_t subAddress, uint8_t count,uint8_t * dest) {
Wire.beginTransmission(address); // 初始化
Wire.write(subAddress); // 1字节的数据位置
uint8_t i = 0;
if (Wire.endTransmission(false) == 0 && Wire.requestFrom(address, (uint8_t)count)) {
while (Wire.available()) {
dest[i++] = Wire.read(); // 读取数据
}
return true;
}
return false;
}
DC Motor模块的软/硬件分析就到此为止了,M5Stack还有很多Module, Unit使用I2C接口通讯,大致原理估计是类似的。
DC Motor模块的电机驱动程序只是一个简单的示例,还有许多可完善的地方,譬如在EV3里的转动指定时间、圈数、度数等功能。
最后,M5Stack的产品设计思路值得我们学习,M5Stack外围设备尽可能采用I2C方式通讯,这样在程序上可以统一数据处理,在硬件上可以通过集线器做简单并联扩展。
万向节即万向接头,英文名称universal joint,是实现变角度动力传递的机件,用于需要改变传动轴线方向的位置
ESP32的DAC函数可以实现真正的模拟输出。
ESP32 没有Arduino输出 PWM 的 analogWrite(pin, value) 方法,取而代之的 ESP32 有一个 LEDC 来实现PWM功能。
本文学习如何使用ESP32开发板来进行多线程的开发。
ESP8266有三种工作模式,分别为:AP,STA,AP混合STA
ESP32有四个SPI外设,分别为SPI0、SPI1、HSPI和VSPI。