M5Stack DC Motor模块详解

DC Motor是M5Stack堆叠模块系列中的一款直流电机驱动模块。本文详细讲解DC Motor的电路原理和程序控制原理。

DC Motor集成MEGA328和L293DD芯片,拥有4个电机驱动通道,采用直流电源输入设计用于功率补充,并通过M-BUS,自动为顶部的M5Core供电。 使用DC Motor模块能够简单快速的驱动RJ12接口编码电机,比如乐高ev3电机。

M5Stack DC Motor模块详解

产品详细介绍:https://docs.m5stack.com/zh_CN/module/lego_plus

一、M5Core与DC Motor的通信

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的电路设计

从上面的分析可以得知,DC Motor作为I2C从设备,需要根据I2C接口的通信来处理是控制电机动作、还是返回电机编码数据等,而DC Motor控制了4个电机,控制就需要8个I/O端口(其中4个支持PWM输出,用于调速),获取编码数据也需要8个I/O端口(能配置中断),DC Motor采用了MEGA328和L293DD的组合(类似于Arduino和L298组合),使用两个L293来驱动4个电机,电路草图如下(省略另一路L293和供电部分)

M5Stack DC Motor模块详解

官方原理图

M5Stack DC Motor模块详解

三、DC Motor的程序分析

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) //编码器数据的总长度
数据存储区示意图

M5Stack DC Motor模块详解


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个电机控制数据过来,
如果要单独控制某个电机(不是第1个)的控制数据,则需要先发送1个字节的数据位置地址过来(第2个电机对应是2,第3个电机对应是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.数据流图

M5Stack DC Motor模块详解

关于编码器程序,老张会另写一文章来专门介绍。


四、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方式通讯,这样在程序上可以统一数据处理,在硬件上可以通过集线器做简单并联扩展。

- 本文由用户 发布,文中观点仅代表作者本人,不代表本站立场。
- 如需转载,请联系作者;如有侵权,请联系本站处理。

2022-06   阅读(480)   评论(0)
 标签: maker ESP32 M5

涨知识
万向节

万向节即万向接头,英文名称universal joint,是实现变角度动力传递的机件,用于需要改变传动轴线方向的位置

评论:
相关文章
ESP32 使用DAC模拟输出完成两路呼吸灯

ESP32的DAC函数可以实现真正的模拟输出。


在 ESP32 上使用 LEDC (PWM)

ESP32 没有Arduino输出 PWM 的 analogWrite(pin, value) 方法,取而代之的 ESP32 有一个 LEDC 来实现PWM功能。


Micropython基于ESP32的多线程开发

本文学习如何使用ESP32开发板来进行多线程的开发。


ESP8266 Arduino WIFI

ESP8266有三种工作模式,分别为:AP,STA,AP混合STA


ESP32 SPI

ESP32有四个SPI外设,分别为SPI0、SPI1、HSPI和VSPI。

搜索
小鹏STEM教研服务

专属教研服务系统,助您构建STEM课程体系,打造一站式教学环境。