【开源】通过MPQ6547A驱动+MA900磁编,控制无刷电机的详细教程

MA900磁编码器真的太好用了 :grinning_face: ,几乎可以实现对电机的任何控制。

经过几天的学习,我轻松实现了用MA900控制无刷电机的正反转、速度、转几圈、并且转到指定的角度。可以看一下实际效果【5 分 20 秒!看懂无刷电机的控制原理与实战-哔哩哔哩】 https://b23.tv/BBr84Xs

接下来,我就把项目用到的电路图和程序上传,有需要的可以看看:eyes:

关于驱动MPQ6547A和磁编MA900的数据手册,大家可以先看一下,对这两个关键的芯片有一个大致了解:

MPQ6547A

MA900

整个项目的连接图如下:我使用的是Arduino R4 WiFi控制板,无刷电机用的是三相无刷直流2804电机。这个电机可以在淘宝上搜一下,有不少商家在卖,大家也可以使用别的无刷电机。

上层楼中间绿色电路板的电路图原理图如下,MPQ6537A通过外置电源直接供电,其实MPQ6547A的VG引脚是可以直接给MA900供电的,但我是第一次做这个东西,怕到时候焊接不好,不好找bug,用的是Arduino上3.3V引脚直接给MA900供电,大家如果想再改进的话,建议直接用VG引脚给MA900供电,这样电路连接还简单点。
MA900的I2C地址的后3位(2进制)由硬件定义,我让这三个引脚通过电阻接地,所以是000,最终I2C地址是0x60!

PCB图

3D预览

源文件请点击下面:backhand_index_pointing_down:

PCB文件.zip (1.6 MB)

然后就是程序部分,本项目采用的是Simple FOC库,这是它的官网:https://docs.simplefoc.com/

通过Simple foc库,即使我们不知道无刷电机的工作原理,也能在很短的时间内实现对无刷电机的控制。STM32和Arduino都支持,因为我这个不是做产品,直接用Arduino实现的。

首先需要在Arduino IDE里面安装一下Simple FOC的库,如下图所示,在左边这个小书架里面搜索Simple FOC,点击安装

接下来:backhand_index_pointing_right:就可以直接写程序控制无刷电机转动了,这个MA900功能非常丰富,I2C通讯带有CRC校验,我曾经花费很长时间把这个关掉一直不成功,因为我这的环境不是那种工业场合,只是简单的控制。

由于不能关掉,所以我这的所有程序的MA900的I2C通讯都带有crc校验。关于CRC校验怎么用,MA900的数据手册上写的也很清楚,这个可以自己查阅。

万事俱备了,我们就先来一个最简单的,不让电机转,先看看传感器有没有工作,当我们转动电机的时候,Arduino上会打印目前的角度值。

// MA900_I2C_QuickRead_CRC.ino
// 适用于 Arduino R4(或任何支持 Wire 的 Arduino)
// 读取 MA900 的 16-bit 角度(寄存器 0x60/0x61),并校验每个字节后的 CRC8(CRCEN = 1)


#include <Wire.h>

const uint8_t MA900_ADDR = 0x60; // 7-bit I2C 地址
const uint8_t ANGLE_LSB_REG = 0x60; // 寄存器起始地址 

/* CRC8 查表:多项式 x^8 + x^2 + x + 1,初始值 0 (
const uint8_t crc8_table[256] = {
  0x00,0x07,0x0e,0x09,0x1c,0x1b,0x12,0x15,0x38,0x3f,0x36,0x31,0x24,0x23,0x2a,0x2d,
  0x70,0x77,0x7e,0x79,0x6c,0x6b,0x62,0x65,0x48,0x4f,0x46,0x41,0x54,0x53,0x5a,0x5d,
  0xe0,0xe7,0xee,0xe9,0xfc,0xfb,0xf2,0xf5,0xd8,0xdf,0xd6,0xd1,0xc4,0xc3,0xca,0xcd,
  0x90,0x97,0x9e,0x99,0x8c,0x8b,0x82,0x85,0xa8,0xaf,0xa6,0xa1,0xb4,0xb3,0xba,0xbd,
  0xc7,0xc0,0xc9,0xce,0xdb,0xdc,0xd5,0xd2,0xff,0xf8,0xf1,0xf6,0xe3,0xe4,0xed,0xea,
  0xb7,0xb0,0xb9,0xbe,0xab,0xac,0xa5,0xa2,0x8f,0x88,0x81,0x86,0x93,0x94,0x9d,0x9a,
  0x27,0x20,0x29,0x2e,0x3b,0x3c,0x35,0x32,0x1f,0x18,0x11,0x16,0x03,0x04,0x0d,0x0a,
  0x57,0x50,0x59,0x5e,0x4b,0x4c,0x45,0x42,0x6f,0x68,0x61,0x66,0x73,0x74,0x7d,0x7a,
  0x89,0x8e,0x87,0x80,0x95,0x92,0x9b,0x9c,0xb1,0xb6,0xbf,0xb8,0xad,0xaa,0xa3,0xa4,
  0xf9,0xfe,0xf7,0xf0,0xe5,0xe2,0xeb,0xec,0xc1,0xc6,0xcf,0xc8,0xdd,0xda,0xd3,0xd4,
  0x69,0x6e,0x67,0x60,0x75,0x72,0x7b,0x7c,0x51,0x56,0x5f,0x58,0x4d,0x4a,0x43,0x44,
  0x19,0x1e,0x17,0x10,0x05,0x02,0x0b,0x0c,0x21,0x26,0x2f,0x28,0x3d,0x3a,0x33,0x34,
  0x4e,0x49,0x40,0x47,0x52,0x55,0x5c,0x5b,0x76,0x71,0x78,0x7f,0x6a,0x6d,0x64,0x63,
  0x3e,0x39,0x30,0x37,0x22,0x25,0x2c,0x2b,0x06,0x01,0x08,0x0f,0x1a,0x1d,0x14,0x13,
  0xae,0xa9,0xa0,0xa7,0xb2,0xb5,0xbc,0xbb,0x96,0x91,0x98,0x9f,0x8a,0x8d,0x84,0x83,
  0xde,0xd9,0xd0,0xd7,0xc2,0xc5,0xcc,0xcb,0xe6,0xe1,0xe8,0xef,0xfa,0xfd,0xf4,0xf3
};

uint8_t crc8(uint8_t crc8_curr, uint8_t data) {
  return crc8_table[crc8_curr ^ data];
}

void setup() {
  Serial.begin(115200);
  while (!Serial) ; // wait for Serial on some boards
  Wire.begin(); // Arduino R4 默认 SDA/SCL 引脚
  // 可尝试加速到 1MHz(MA900 支持),如果你的硬件/线长允许:
  // Wire.setClock(1000000);
  Wire.setClock(400000); // 先用 400kHz,稳定
  Serial.println("MA900 I2C Quick Read with CRC example");
}

void loop() {
  // Quick Read: 请求 ANGLE LSB(0x60) 和 ANGLE MSB(0x61)
  // 当 CRCEN=1,设备会在每个字节后附加 CRC8 -> 总共 4 字节: LSB, CRC0, MSB, CRC1
  const uint8_t toRead = 4;
  Wire.requestFrom((uint8_t)MA900_ADDR, toRead); // Quick Read: 直接读(不先写寄存器)
  unsigned long t0 = millis();
  while (Wire.available() < toRead && (millis() - t0) < 50) {
    // 等待最多 50ms
  }
  if (Wire.available() < toRead) {
    Serial.println("I2C read timeout / not enough bytes");
    delay(200);
    return;
  }

  uint8_t angle_lsb = Wire.read();
  uint8_t crc0       = Wire.read();
  uint8_t angle_msb = Wire.read();
  uint8_t crc1       = Wire.read();

  // 逐个寄存器校验 CRC(按手册 Quick Read 的 CRC 算法)
  bool crc_ok0 = false, crc_ok1 = false;
  // 对第 0 个寄存器(addr = 0x60)
  {
    uint8_t c = 0;
    c = crc8(c, (MA900_ADDR << 1) | 0x01); // (addr<<1)+1 : master is in read mode for Quick Read
    c = crc8(c, ANGLE_LSB_REG + 0);        // register address (0x60 + i)
    c = crc8(c, angle_lsb);
    crc_ok0 = (c == crc0);
  }
  // 对第 1 个寄存器(addr = 0x61)
  {
    uint8_t c = 0;
    c = crc8(c, (MA900_ADDR << 1) | 0x01);
    c = crc8(c, ANGLE_LSB_REG + 1);
    c = crc8(c, angle_msb);
    crc_ok1 = (c == crc1);
  }

  if (!crc_ok0 || !crc_ok1) {
    Serial.print("CRC fail: crc0=");
    Serial.print(crc0, HEX);
    Serial.print(" calc0=");
    {
      uint8_t c = 0;
      c = crc8(c, (MA900_ADDR << 1) | 0x01);
      c = crc8(c, ANGLE_LSB_REG + 0);
      c = crc8(c, angle_lsb);
      Serial.print(c, HEX);
    }
    Serial.print(" | crc1=");
    Serial.print(crc1, HEX);
    Serial.print(" calc1=");
    {
      uint8_t c = 0;
      c = crc8(c, (MA900_ADDR << 1) | 0x01);
      c = crc8(c, ANGLE_LSB_REG + 1);
      c = crc8(c, angle_msb);
      Serial.print(c, HEX);
    }
    Serial.println();
    // 如果你需要即便 CRC 错也继续使用原始数据,取消下面的 return
    delay(200);
    return;
  }

  uint16_t angle_raw = ((uint16_t)angle_msb << 8) | angle_lsb;
  float angle_deg = (float)angle_raw * 360.0f / 65536.0f; // 0..65535 -> 0..360°


  Serial.print("绝对角度值:");
  Serial.print(angle_deg, 4);
    Serial.println("°");

  delay(200); // 读频率随你需求调整,手册建议测率在 kHz 级,注意不要太频繁触发 I2C 总线冲突
}

然后就电机转起来,更改 motor.move();括号里面的参数,就能改变电机转动的速度。

(CRC校验有两种,一种是传统的/一种是快读方式,下面这个程序用的快读方式,这样能简化很多程序步骤)

  #include <SimpleFOC.h>
#include <Wire.h>
//以恒定的速度转动 闭环控制 
// ================== MA900 传感器实现 ==================
const uint8_t MA900_ADDR = 0x60;
const uint8_t MA900_ANGLE_REG = 0x60;   // Quick Read 起始寄存器

// CRC8 查表(多项式 0x07)
const uint8_t crc8_table[256] = {
  0x00,0x07,0x0E,0x09,0x1C,0x1B,0x12,0x15,0x38,0x3F,0x36,0x31,0x24,0x23,0x2A,0x2D,
  0x70,0x77,0x7E,0x79,0x6C,0x6B,0x62,0x65,0x48,0x4F,0x46,0x41,0x54,0x53,0x5A,0x5D,
  0xE0,0xE7,0xEE,0xE9,0xFC,0xFB,0xF2,0xF5,0xD8,0xDF,0xD6,0xD1,0xC4,0xC3,0xCA,0xCD,
  0x90,0x97,0x9E,0x99,0x8C,0x8B,0x82,0x85,0xA8,0xAF,0xA6,0xA1,0xB4,0xB3,0xBA,0xBD,
  0xC7,0xC0,0xC9,0xCE,0xDB,0xDC,0xD5,0xD2,0xFF,0xF8,0xF1,0xF6,0xE3,0xE4,0xED,0xEA,
  0xB7,0xB0,0xB9,0xBE,0xAB,0xAC,0xA5,0xA2,0x8F,0x88,0x81,0x86,0x93,0x94,0x9D,0x9A,
  0x27,0x20,0x29,0x2E,0x3B,0x3C,0x35,0x32,0x1F,0x18,0x11,0x16,0x03,0x04,0x0D,0x0A,
  0x57,0x50,0x59,0x5E,0x4B,0x4C,0x45,0x42,0x6F,0x68,0x61,0x66,0x73,0x74,0x7D,0x7A,
  0x89,0x8E,0x87,0x80,0x95,0x92,0x9B,0x9C,0xB1,0xB6,0xBF,0xB8,0xAD,0xAA,0xA3,0xA4,
  0xF9,0xFE,0xF7,0xF0,0xE5,0xE2,0xEB,0xEC,0xC1,0xC6,0xCF,0xC8,0xDD,0xDA,0xD3,0xD4,
  0x69,0x6E,0x67,0x60,0x75,0x72,0x7B,0x7C,0x51,0x56,0x5F,0x58,0x4D,0x4A,0x43,0x44,
  0x19,0x1E,0x17,0x10,0x05,0x02,0x0B,0x0C,0x21,0x26,0x2F,0x28,0x3D,0x3A,0x33,0x34,
  0x4E,0x49,0x40,0x47,0x52,0x55,0x5C,0x5B,0x76,0x71,0x78,0x7F,0x6A,0x6D,0x64,0x63,
  0x3E,0x39,0x30,0x37,0x22,0x25,0x2C,0x2B,0x06,0x01,0x08,0x0F,0x1A,0x1D,0x14,0x13,
  0xAE,0xA9,0xA0,0xA7,0xB2,0xB5,0xBC,0xBB,0x96,0x91,0x98,0x9F,0x8A,0x8D,0x84,0x83,
  0xDE,0xD9,0xD0,0xD7,0xC2,0xC5,0xCC,0xCB,0xE6,0xE1,0xE8,0xEF,0xFA,0xFD,0xF4,0xF3
};

// 自定义传感器类,继承 SimpleFOC 的 Sensor
class MA900Sensor : public Sensor {
public:
  MA900Sensor(uint8_t addr = MA900_ADDR) : address(addr) {}

  // 公共 init:内部调用受保护的 Sensor::init()
  void init() {
    // I2C 初始化(你外面已经 Wire.begin() 过也没关系)
    Wire.begin();
    Wire.setClock(400000);  // 400k 稳一点

    // 设置一次寄存器指针到角度寄存器,这样 Quick Read 从0x60开始
    Wire.beginTransmission(address);
    Wire.write(MA900_ANGLE_REG);
    Wire.endTransmission();

    last_valid_angle = 0.0f;
    Sensor::init();   // 让基类把 timestamp 啥的初始化好
  }

  // 核心:返回机械角度(0~2PI)
  float getSensorAngle() override {
    float ang;
    if (readAngleRad(ang)) {
      last_valid_angle = ang;
    }
    // 如果这次读失败,就返回上一次的,避免乱跳
    return last_valid_angle;
  }

private:
  uint8_t address;
  float last_valid_angle = 0.0f;

  bool readAngleRad(float &angle_rad) {
    Wire.requestFrom(address, (uint8_t)4);
    if (Wire.available() < 4) {
      return false;
    }

    uint8_t lsb  = Wire.read();
    uint8_t crc0 = Wire.read();
    uint8_t msb  = Wire.read();
    uint8_t crc1 = Wire.read();

    // ---- CRC 校验 LSB ----
    uint8_t c0 = 0;
    c0 = crc8_table[c0 ^ ((address << 1) | 1)];       // 读地址
    c0 = crc8_table[c0 ^ MA900_ANGLE_REG];           // reg 0x60
    c0 = crc8_table[c0 ^ lsb];

    // ---- CRC 校验 MSB ----
    uint8_t c1 = 0;
    c1 = crc8_table[c1 ^ ((address << 1) | 1)];
    c1 = crc8_table[c1 ^ (MA900_ANGLE_REG + 1)];     // reg 0x61
    c1 = crc8_table[c1 ^ msb];

    if (c0 != crc0 || c1 != crc1) {
      // CRC 不通过,丢弃本次数据
      return false;
    }

    uint16_t raw = ((uint16_t)msb << 8) | lsb;   // 0~65535
    angle_rad = raw * _2PI / 65536.0f;           // 映射到 0~2PI
    return true;
  }
};

// ================== 电机 & 驱动 ==================
// 7 极对 BLDC
BLDCMotor motor = BLDCMotor(7);

// 3PWM 驱动:相位 A/B/C 用 9/10/11
BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11);

// 使能脚 5、6
const int ENA_PIN = 5;
const int ENB_PIN = 6;

// 我们的 MA900 传感器实例
MA900Sensor ma900;

// 目标速度(rad/s)
float target_velocity = 3.0f;   // 你可以改成 2.0 / 3.0 先试

unsigned long last_print = 0;

void setup() {
  Serial.begin(115200);

  // I2C 初始化(保证 Wire 可用,MA900Sensor 内部也会再调用一次没关系)
  Wire.begin();
  Wire.setClock(400000);

  // 使能脚拉高
  pinMode(ENA_PIN, OUTPUT);
  pinMode(ENB_PIN, OUTPUT);
  digitalWrite(ENA_PIN, HIGH);
  digitalWrite(ENB_PIN, HIGH);

  // 先初始化传感器
  ma900.init();

  // 驱动初始化
  driver.voltage_power_supply = 5;   // 按你的电源改,比如 12V
  driver.init();

  // 将驱动和传感器链接到电机
  motor.linkDriver(&driver);
  motor.linkSensor(&ma900);

  // ========== 电机参数配置 ==========
  // 闭环速度控制
  motor.controller = MotionControlType::velocity;

  // 限制电压和速度,先温柔一点
  motor.voltage_limit  = 3.0f;   // 电压上限,嫌力矩大可以改 2.0
  motor.velocity_limit = 500.0f;  // 最大速度上限,单位 rad/s

  // 速度环 PID 参数,可以后面慢慢调
  motor.PID_velocity.P = 0.5f;
  motor.PID_velocity.I = 20.0f;
  motor.PID_velocity.D = 0.0f;
  motor.PID_velocity.output_ramp = 1000.0f;  // 输出变化速度限制
  motor.LPF_velocity.Tf = 0.01f;             // 速度滤波时间常数

  // 如果你要用角度控制时会用到
  motor.P_angle.P = 20.0f;

  // FOC 调制方式
  motor.foc_modulation = FOCModulationType::SinePWM;

  // 初始化电机
  motor.init();

  // 关键:FOC 对齐(这里会用到 MA900 的角度)
  Serial.println("Running FOC init...");
  motor.initFOC();
  Serial.println("FOC init done. Closed-loop velocity control with MA900 ready!");
}

void loop() {
  // 1. FOC 算磁场方向(闭环)
  motor.loopFOC();

  // 2. 闭环速度控制:目标速度是 target_velocity
  motor.move(target_velocity);

  // 3. 打印一点调试信息
  if (millis() - last_print > 200) {
    last_print = millis();
    Serial.print("shaft_angle(deg)=");
    Serial.print(motor.shaft_angle * 180.0f / PI, 1);
    Serial.print("  shaft_velocity(rad/s)=");
    Serial.print(motor.shaft_velocity, 2);
    Serial.print("  target_vel=");
    Serial.println(target_velocity, 2);
  }
}

到现在为止,好像还没有展示出无刷电机+磁编码器的优势

那接下来上点难度:eyes:,让他准确的转③圈。

#include <SimpleFOC.h>
#include <Wire.h>

// ================== MA900 传感器实现(带 CRC) ==================
//完全闭环的情况下让电机转三圈 通过MA900
const uint8_t MA900_ADDR      = 0x60;   // 7bit 地址
const uint8_t MA900_ANGLE_REG = 0x60;   // Quick Read 起始寄存器

// CRC8 查表(多项式 0x07)
const uint8_t crc8_table[256] = {
  0x00,0x07,0x0E,0x09,0x1C,0x1B,0x12,0x15,0x38,0x3F,0x36,0x31,0x24,0x23,0x2A,0x2D,
  0x70,0x77,0x7E,0x79,0x6C,0x6B,0x62,0x65,0x48,0x4F,0x46,0x41,0x54,0x53,0x5A,0x5D,
  0xE0,0xE7,0xEE,0xE9,0xFC,0xFB,0xF2,0xF5,0xD8,0xDF,0xD6,0xD1,0xC4,0xC3,0xCA,0xCD,
  0x90,0x97,0x9E,0x99,0x8C,0x8B,0x82,0x85,0xA8,0xAF,0xA6,0xA1,0xB4,0xB3,0xBA,0xBD,
  0xC7,0xC0,0xC9,0xCE,0xDB,0xDC,0xD5,0xD2,0xFF,0xF8,0xF1,0xF6,0xE3,0xE4,0xED,0xEA,
  0xB7,0xB0,0xB9,0xBE,0xAB,0xAC,0xA5,0xA2,0x8F,0x88,0x81,0x86,0x93,0x94,0x9D,0x9A,
  0x27,0x20,0x29,0x2E,0x3B,0x3C,0x35,0x32,0x1F,0x18,0x11,0x16,0x03,0x04,0x0D,0x0A,
  0x57,0x50,0x59,0x5E,0x4B,0x4C,0x45,0x42,0x6F,0x68,0x61,0x66,0x73,0x74,0x7D,0x7A,
  0x89,0x8E,0x87,0x80,0x95,0x92,0x9B,0x9C,0xB1,0xB6,0xBF,0xB8,0xAD,0xAA,0xA3,0xA4,
  0xF9,0xFE,0xF7,0xF0,0xE5,0xE2,0xEB,0xEC,0xC1,0xC6,0xCF,0xC8,0xDD,0xDA,0xD3,0xD4,
  0x69,0x6E,0x67,0x60,0x75,0x72,0x7B,0x7C,0x51,0x56,0x5F,0x58,0x4D,0x4A,0x43,0x44,
  0x19,0x1E,0x17,0x10,0x05,0x02,0x0B,0x0C,0x21,0x26,0x2F,0x28,0x3D,0x3A,0x33,0x34,
  0x4E,0x49,0x40,0x47,0x52,0x55,0x5C,0x5B,0x76,0x71,0x78,0x7F,0x6A,0x6D,0x64,0x63,
  0x3E,0x39,0x30,0x37,0x22,0x25,0x2C,0x2B,0x06,0x01,0x08,0x0F,0x1A,0x1D,0x14,0x13,
  0xAE,0xA9,0xA0,0xA7,0xB2,0xB5,0xBC,0xBB,0x96,0x91,0x98,0x9F,0x8A,0x8D,0x84,0x83,
  0xDE,0xD9,0xD0,0xD7,0xC2,0xC5,0xCC,0xCB,0xE6,0xE1,0xE8,0xEF,0xFA,0xFD,0xF4,0xF3
};

inline uint8_t crc8_update(uint8_t crc, uint8_t data) {
  return crc8_table[crc ^ data];
}

// 自定义传感器类,接入 SimpleFOC
class MA900Sensor : public Sensor {
public:
  MA900Sensor(uint8_t addr = MA900_ADDR) : address(addr) {}

  void init() {
    // I2C 在外面也会 init,这里再设一次也没关系
    Wire.begin();
    Wire.setClock(400000);

    // 可选:把内部指针指到 0x60,一般 Quick Read 也不用,但这样更保险
    Wire.beginTransmission(address);
    Wire.write(MA900_ANGLE_REG);
    Wire.endTransmission();

    last_valid_angle = 0.0f;
    Sensor::init();   // 让 SimpleFOC 初始化时间戳等
  }

  // 返回机械角度(0~2PI)
  float getSensorAngle() override {
    float ang;
    if (readAngleRadCRC(ang)) {
      last_valid_angle = ang;
    }
    // CRC 错/读不到时,返回上一次的值,避免跳变
    return last_valid_angle;
  }

private:
  uint8_t address;
  float   last_valid_angle = 0.0f;

  bool readAngleRadCRC(float &angle_rad) {
    // MA900 Quick Read: 直接读 4 字节:LSB, CRC0, MSB, CRC1
    Wire.requestFrom((int)address, 4);
    if (Wire.available() < 4) {
      return false;
    }

    uint8_t lsb  = Wire.read();
    uint8_t crc0 = Wire.read();
    uint8_t msb  = Wire.read();
    uint8_t crc1 = Wire.read();

    // CRC 校验 LSB:addr(读) + reg(0x60) + lsb
    uint8_t c0 = 0;
    c0 = crc8_update(c0, (address << 1) | 1);      // 读地址
    c0 = crc8_update(c0, MA900_ANGLE_REG);        // 0x60
    c0 = crc8_update(c0, lsb);

    // CRC 校验 MSB:addr(读) + reg+1(0x61) + msb
    uint8_t c1 = 0;
    c1 = crc8_update(c1, (address << 1) | 1);
    c1 = crc8_update(c1, (uint8_t)(MA900_ANGLE_REG + 1)); // 0x61
    c1 = crc8_update(c1, msb);

    if (c0 != crc0 || c1 != crc1) {
      // CRC 错,丢掉本次数据
      return false;
    }

    uint16_t raw = ((uint16_t)msb << 8) | lsb;    // 0~65535
    angle_rad = raw * _2PI / 65536.0f;            // 映射到 0~2PI
    return true;
  }
};

// ================== 电机 & 驱动 ==================
BLDCMotor motor = BLDCMotor(7);                 // 7 极对
BLDCDriver3PWM driver = BLDCDriver3PWM(9, 10, 11);

const int ENA_PIN = 5;
const int ENB_PIN = 6;

MA900Sensor ma900;

// 目标:3 圈
const float TARGET_TURNS       = 3.0f;
// 巡航速度
const float CRUISE_VEL         = 5.0f;  // rad/s
// 从多少圈开始减速(可以自己调)
const float DECEL_START_TURNS  = 2.7f;

// 软起步 / 软刹车
float current_vel = 0.0f;
const float VEL_STEP = 0.1f;   // 每次 loop 速度最大变化(越小越温柔)

// 统计圈数:用 shaft_angle 差值来算
float start_shaft_angle = 0.0f;

// 状态
bool  motion_done  = false;
float final_turns  = 0.0f;

unsigned long last_print = 0;

void setup() {
  Serial.begin(115200);

  // I2C
  Wire.begin();
  Wire.setClock(400000);

  // EN 脚
  pinMode(ENA_PIN, OUTPUT);
  pinMode(ENB_PIN, OUTPUT);
  digitalWrite(ENA_PIN, HIGH);
  digitalWrite(ENB_PIN, HIGH);

  // 初始化 MA900(带 CRC)
  ma900.init();

  // 驱动
  driver.voltage_power_supply = 12;   // 根据你的电源电压改,比如 12V
  driver.init();

  // 关联到电机
  motor.linkDriver(&driver);
  motor.linkSensor(&ma900);

  // 真闭环:速度模式
  motor.controller     = MotionControlType::velocity;
  motor.voltage_limit  = 3.0f;   // 觉得力矩大可以改小一点 2.0f
  motor.velocity_limit = 50.0f;

  // 速度环 PID,可以后面慢慢调
  motor.PID_velocity.P = 0.5f;
  motor.PID_velocity.I = 20.0f;
  motor.PID_velocity.D = 0.0f;
  motor.PID_velocity.output_ramp = 1000.0f;
  motor.LPF_velocity.Tf = 0.01f;

  // 角度环先留着,以后玩位置控制用
  motor.P_angle.P = 20.0f;

  motor.foc_modulation = FOCModulationType::SinePWM;

  motor.init();

  Serial.println("Running FOC init (会轻微抖动校准)...");
  motor.initFOC();         // ★ 必然会小幅正反扭一扭,这是在标定零点和方向
  Serial.println("FOC init done. Closed-loop CRC 3-turn test.");

  // 标定完成后,把“开始的 shaft_angle”记录下来
  start_shaft_angle = motor.shaft_angle;

  current_vel = 0.0f;
}

void loop() {
  if (!motion_done) {
    // 1. 真闭环:用 MA900 + CRC 算磁场方向
    motor.loopFOC();

    // 2. 计算已经转了多少圈(用 shaft_angle 和起点差值)
    float shaft = motor.shaft_angle;    // 连续弧度
    float turns = (shaft - start_shaft_angle) / _2PI;

    // 3. 根据圈数决定目标速度:先匀速,再减速,再停
    float target_vel;
    float abs_turns = fabs(turns);

    if (abs_turns < DECEL_START_TURNS) {
      // 0 ~ DECEL_START_TURNS:用巡航速度
      target_vel = CRUISE_VEL;
    } else if (abs_turns < TARGET_TURNS) {
      // 减速区间:线性从 CRUISE_VEL 降到 0
      float k = (TARGET_TURNS - abs_turns) / (TARGET_TURNS - DECEL_START_TURNS); // 0~1
      if (k < 0) k = 0;
      target_vel = CRUISE_VEL * k;
    } else {
      target_vel = 0.0f;
    }

    // 4. 软起步/软刹车:current_vel 慢慢追 target_vel,避免突然冲一下
    if (current_vel < target_vel) {
      current_vel += VEL_STEP;
      if (current_vel > target_vel) current_vel = target_vel;
    } else if (current_vel > target_vel) {
      current_vel -= VEL_STEP;
      if (current_vel < target_vel) current_vel = target_vel;
    }

    // 5. 当目标速度为 0 且当前速度已经很小,同时圈数 >= 3,就认为完成
    if (target_vel == 0.0f && fabs(current_vel) < 0.05f && abs_turns >= TARGET_TURNS) {
      current_vel = 0.0f;
      final_turns = turns;
      motion_done = true;

      // 停止控制并彻底断电
      motor.move(0.0f);
      motor.disable();
      driver.disable();
      digitalWrite(ENA_PIN, LOW);
      digitalWrite(ENB_PIN, LOW);

      Serial.println("=== 3 turns done, motor fully disabled (CRC OK) ===");
    } else {
      // 6. 正常闭环速度控制
      motor.move(current_vel);
    }

    // 7. 串口打印
    if (millis() - last_print > 200) {
      last_print = millis();
      Serial.print("shaft_angle(deg)=");
      Serial.print(shaft * 180.0f / PI, 1);
      Serial.print("  turns=");
      Serial.print(turns, 3);
      Serial.print("  current_vel=");
      Serial.println(current_vel, 2);
    }

  } else {
    // 已经完成三圈:不再调用 loopFOC / move,电机完全放松,只打印最终圈数
    if (millis() - last_print > 500) {
      last_print = millis();
      Serial.print("turns(final)=");
      Serial.println(final_turns, 3);
    }
  }
}


接下来

让电机转到多少度,并且在这个角度停留,即使是你把它拧一拧:crossed_fingers:,它还能自动回正,通过串口通讯控制它的角度,比如你给他发0,它就转到0度的位置。

#include <SimpleFOC.h>
#include <Wire.h>

#include "CRC.h"
// ================== MA900 快读 + CRC ==================
//可以完美运行版本 非常顺滑(启动的一刻钟会稍微震荡)当给串口角度时,它就能转到这个角度 后面即使是转到其它角度,他依旧能回到这个角度 
#define MA900_ADDR 0x60

const uint8_t crc8_table[256] = {
  0x00,0x07,0x0E,0x09,0x1C,0x1B,0x12,0x15,0x38,0x3F,0x36,0x31,0x24,0x23,0x2A,0x2D,
  0x70,0x77,0x7E,0x79,0x6C,0x6B,0x62,0x65,0x48,0x4F,0x46,0x41,0x54,0x53,0x5A,0x5D,
  0xE0,0xE7,0xEE,0xE9,0xFC,0xFB,0xF2,0xF5,0xD8,0xDF,0xD6,0xD1,0xC4,0xC3,0xCA,0xCD,
  0x90,0x97,0x9E,0x99,0x8C,0x8B,0x82,0x85,0xA8,0xAF,0xA6,0xA1,0xB4,0xB3,0xBA,0xBD,
  0xC7,0xC0,0xC9,0xCE,0xDB,0xDC,0xD5,0xD2,0xFF,0xF8,0xF1,0xF6,0xE3,0xE4,0xED,0xEA,
  0xB7,0xB0,0xB9,0xBE,0xAB,0xAC,0xA5,0xA2,0x8F,0x88,0x81,0x86,0x93,0x94,0x9D,0x9A,
  0x27,0x20,0x29,0x2E,0x3B,0x3C,0x35,0x32,0x1F,0x18,0x11,0x16,0x03,0x04,0x0D,0x0A,
  0x57,0x50,0x59,0x5E,0x4B,0x4C,0x45,0x42,0x6F,0x68,0x61,0x66,0x73,0x74,0x7D,0x7A,
  0x89,0x8E,0x87,0x80,0x95,0x92,0x9B,0x9C,0xB1,0xB6,0xBF,0xB8,0xAD,0xAA,0xA3,0xA4,
  0xF9,0xFE,0xF7,0xF0,0xE5,0xE2,0xEB,0xEC,0xC1,0xC6,0xCF,0xC8,0xDD,0xDA,0xD3,0xD4,
  0x69,0x6E,0x67,0x60,0x75,0x72,0x7B,0x7C,0x51,0x56,0x5F,0x58,0x4D,0x4A,0x43,0x44,
  0x19,0x1E,0x17,0x10,0x05,0x02,0x0B,0x0C,0x21,0x26,0x2F,0x28,0x3D,0x3A,0x33,0x34,
  0x4E,0x49,0x40,0x47,0x52,0x55,0x5C,0x5B,0x76,0x71,0x78,0x7F,0x6A,0x6D,0x64,0x63,
  0x3E,0x39,0x30,0x37,0x22,0x25,0x2C,0x2B,0x06,0x01,0x08,0x0F,0x1A,0x1D,0x14,0x13,
  0xAE,0xA9,0xA0,0xA7,0xB2,0xB5,0xBC,0xBB,0x96,0x91,0x98,0x9F,0x8A,0x8D,0x84,0x83,
  0xDE,0xD9,0xD0,0xD7,0xC2,0xC5,0xCC,0xCB,0xE6,0xE1,0xE8,0xEF,0xFA,0xFD,0xF4,0xF3
};

uint8_t crc8_calc(uint8_t crc, uint8_t data) {
  return crc8_table[crc ^ data];
}

// 读取 MA900 绝对角(rad),返回 true=OK
bool ma900_read_angle_rad(float &rad) {
  Wire.requestFrom(MA900_ADDR, 4);
  if (Wire.available() < 4) {
    return false;
  }

  uint8_t L  = Wire.read();
  uint8_t C0 = Wire.read();
  uint8_t H  = Wire.read();
  uint8_t C1 = Wire.read();

  // CRC0
  uint8_t crc0 = 0;
  crc0 = crc8_calc(crc0, (MA900_ADDR<<1)|1);
  crc0 = crc8_calc(crc0, 0x60);
  crc0 = crc8_calc(crc0, L);
  if (crc0 != C0) {
    return false;
  }

  // CRC1
  uint8_t crc1 = 0;
  crc1 = crc8_calc(crc1, (MA900_ADDR<<1)|1);
  crc1 = crc8_calc(crc1, 0x61);
  crc1 = crc8_calc(crc1, H);
  if (crc1 != C1) {
    return false;
  }

  uint16_t raw = ((uint16_t)H << 8) | L;
  rad = raw * _2PI / 65536.0f;   // 0~2π
  return true;
}

// ================== GenericSensor 回调(只要角度) ==================

float ma900_last_angle = 0.0f;

// 给 SimpleFOC 用的角度回调(rad)
float ma900_getAngle() {
  float r;
  if (ma900_read_angle_rad(r)) {
    ma900_last_angle = r;
  }
  return ma900_last_angle;
}

// SimpleFOC 通用传感器(只传角度回调)
GenericSensor sensor = GenericSensor(ma900_getAngle);

// ================== 电机 & 驱动 ==================

BLDCMotor motor(7);                 // 7 极对数
BLDCDriver3PWM driver(9, 10, 11);   // 3PWM:9/10/11

// 驱动使能脚
const int ENA_PIN = 5;
const int ENB_PIN = 6;

// 位置外环参数:根据误差生成目标速度
const float Kp_pos  = 1.0f;   // ☆ 原来 1.5,稍微减小一点
const float VEL_MAX = 5.0f;   // 最大目标速度 (rad/s)

// 串口命令的“期望绝对角”(MA900坐标系,rad)
float cmd_abs_angle    = 0.0f;
// 实际用于控制的“平滑后的目标绝对角”(rad)
float target_abs_angle = 0.0f;

unsigned long last_print = 0;

// 状态机:上电先 HOMING,之后 READY(等待串口命令)/MOVING
enum { STATE_HOMING = 0, STATE_READY = 1, STATE_MOVING = 2 };
int state = STATE_HOMING;

// 回零判定
const float HOMING_TOL_RAD = 1.0f * PI / 180.0f;  // 1 度误差内算到位
const unsigned long HOMING_STABLE_MS = 500;       // 连续 500ms 在范围内才算真正到位
unsigned long homing_in_tol_since = 0;

// 每次循环目标角度最多变化的角度:1度(防止刚开始猛冲)
const float TARGET_SLEW_STEP_RAD = 1.0f * PI / 180.0f;  // 1 度/循环

// 角度 wrap 到 [-PI, PI]
float wrapToPi(float x) {
  while (x >  PI) x -= _2PI;
  while (x < -PI) x += _2PI;
  return x;
}

// 平滑把 target_abs_angle 追到 cmd_abs_angle(限制每次变化幅度)
void updateSmoothTarget() {
  float diff = wrapToPi(cmd_abs_angle - target_abs_angle);
  if (fabs(diff) <= TARGET_SLEW_STEP_RAD) {
    target_abs_angle = cmd_abs_angle;
  } else {
    target_abs_angle += (diff > 0 ? TARGET_SLEW_STEP_RAD : -TARGET_SLEW_STEP_RAD);
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Wire.setClock(400000);

  Serial.println("=== MA900 + SimpleFOC ===");
  Serial.println("功能:上电自动回到 MA900 絶対0度,然后串口输入角度(单位:度),电机转到该绝对角度。");
  Serial.println("(已加入目标角平滑,减少刚开始的震荡)");

  // 简单测一下面 MA900
  delay(200);
  Serial.println("MA900 quick test:");
  for (int i = 0; i < 5; i++) {
    float r;
    if (ma900_read_angle_rad(r)) {
      Serial.print("  ANGLE = ");
      Serial.print(r * 180.0f / PI, 3);
      Serial.println(" deg");
    } else {
      Serial.println("  MA900 READ FAIL");
    }
    delay(50);
  }
  Serial.println("---- end of MA900 test ----");

  // 打开驱动使能(5、6 拉高)
  pinMode(ENA_PIN, OUTPUT);
  pinMode(ENB_PIN, OUTPUT);
  digitalWrite(ENA_PIN, HIGH);
  digitalWrite(ENB_PIN, HIGH);

  // 驱动:5V 供电
  driver.voltage_power_supply = 5.0f;
  driver.init();

  // 传感器 & 电机
  sensor.init();
  motor.linkSensor(&sensor);
  motor.linkDriver(&driver);

  // 速度闭环(外环自己做位置控制)
  motor.controller = MotionControlType::velocity;
  motor.voltage_limit  = 2.0f;      // 别超过供电电压
  motor.velocity_limit = VEL_MAX;

  // 速度环 PID(根据你之前“顺滑”的体验微调)
  motor.PID_velocity.P  = 0.3f;
  motor.PID_velocity.I  = 8.0f;
  motor.PID_velocity.D  = 0;
  motor.LPF_velocity.Tf = 0.02f;

  motor.init();

  Serial.println("Init FOC...");
  // 如果发现转向反了,可以改成 CCW 再试:
  // motor.sensor_direction = Direction::CCW;
  motor.initFOC();
  Serial.println("FOC ready.");

  // 上电之后,先把串口命令角 & 目标角都设为 0 度(homing)
  cmd_abs_angle    = 0.0f;
  target_abs_angle = 0.0f;
  state = STATE_HOMING;
  homing_in_tol_since = 0;
}

// 处理串口命令:读取一行,将其当“目标绝对角度(度)”
void handleSerialCommand() {
  if (!Serial.available()) return;

  String line = Serial.readStringUntil('\n');
  line.trim();
  if (line.length() == 0) return;

  float deg = line.toFloat();  // 允许负数,比如 -90
  float new_cmd = deg * PI / 180.0f;

  // 正规化到 [0, 2π),方便看(可选)
  while (new_cmd >= _2PI) new_cmd -= _2PI;
  while (new_cmd < 0.0f)  new_cmd += _2PI;

  cmd_abs_angle = new_cmd;  // ☆ 只改命令角,由 updateSmoothTarget() 慢慢追

  Serial.print("[CMD] New cmd abs angle = ");
  Serial.print(deg, 3);
  Serial.print(" deg  (normalized = ");
  Serial.print(cmd_abs_angle * 180.0f / PI, 3);
  Serial.println(" deg)");

  state = STATE_MOVING;  // 进入“按命令转动”状态
}

void loop() {
  // FOC 内环
  motor.loopFOC();

  // 处理串口命令(随时可以发角度命令)
  handleSerialCommand();

  // 更新平滑后的目标绝对角
  updateSmoothTarget();

  // 读取当前 MA900 绝对角
  float abs_angle = ma900_getAngle();        // 0~2π
  float error = wrapToPi(target_abs_angle - abs_angle);  // 误差 [-π, π]

  // ---- 状态机:上电先 HOMING,之后 READY/MOVING ----
  if (state == STATE_HOMING) {
    // 检查是否已经回到 0 度附近
    if (fabs(error) < HOMING_TOL_RAD) {
      if (homing_in_tol_since == 0) {
        homing_in_tol_since = millis();
      } else if (millis() - homing_in_tol_since > HOMING_STABLE_MS) {
        // 连续一段时间都在误差范围内,认为回正完成
        state = STATE_READY;
        Serial.print("[HOMED] abs_angle = ");
        Serial.print(abs_angle * 180.0f / PI, 3);
        Serial.println(" deg  (已回到绝对零度附近)");
      }
    } else {
      homing_in_tol_since = 0;  // 超出范围,重新计时
    }
  }

  // 位置外环:根据误差生成目标速度
  float vel_target = Kp_pos * error;

  // 限制最大速度
  if (vel_target >  VEL_MAX) vel_target =  VEL_MAX;
  if (vel_target < -VEL_MAX) vel_target = -VEL_MAX;

  // 误差很小时,就停下(避免抖动):这里 1 度
  if (fabs(error) < (1.0f * PI / 180.0f)) {
    vel_target = 0.0f;
  }

  // 速度闭环执行
  motor.move(vel_target);

  // 串口调试输出
  if (millis() - last_print > 200) {
    last_print = millis();

    Serial.print("STATE=");
    if (state == STATE_HOMING) Serial.print("HOMING");
    else if (state == STATE_READY) Serial.print("READY");
    else Serial.print("MOVING");

    Serial.print("  abs(deg)=");
    Serial.print(abs_angle * 180.0f / PI, 2);

    Serial.print("  target_abs(deg)=");
    Serial.print(target_abs_angle * 180.0f / PI, 2);

    Serial.print("  cmd_abs(deg)=");
    Serial.print(cmd_abs_angle * 180.0f / PI, 2);

    Serial.print("  error(deg)=");
    Serial.print(error * 180.0f / PI, 2);

    Serial.print("  vel_target(rad/s)=");
    Serial.println(vel_target, 2);
  }
}


好了,这就是关于无刷电机+MA900磁编码器能实现的一些高级控制方式,希望对你有帮助。 :grinning_face:

2 个赞

厉害!先点赞~

1 个赞

在这发现百万up :saluting_face: