【2026 机器人模块大赛】+力反馈旋钮7-最终成品汇报

ESP32+MP6543+MA600A 28mm 云台无刷 FOC 力反馈旋钮

1. 作品简介

(1)基础信息

作品名称:基于 SimpleFOC 的云台无刷电机力反馈闭环控制器
作 者:MetalHeart
作品照片:


(2)作品功能介绍

  • 本作品以 ESP32 为主控,采用 MP6543 三相无刷驱动、MA600A TMR 磁编码器搭建完整 FOC 磁场定向闭环控制系统。可实现电机角度闭环锁定、串口实时读取转子绝对角度、串口指令修改目标定位角度;内置硬件过流、过热故障检测保护;支持 360° 全量程高精度角度采集,适用于小型云台、力反馈旋钮、小型关节伺服等场景,解决传统开环电机抖动、定位精度差问题。
  • ESP32 开启 BLE 蓝牙 HID 多媒体键盘模式,电脑蓝牙配对设备后,实时读取 MA600A 输出的电机角度变化差值;电机正向旋转发送音量增大多媒体按键指令,反向旋转发送音量减小指令,实现旋钮式音量调节。

(3)物料清单及简介

  1. ESP32-S3 主控:核心运算单元,运行 SimpleFOC 算法、SPI 读取编码器、串口交互;
  2. MP6543 三相无刷驱动芯片:集成低侧电流采样,驱动 28mm 云台无刷电机;
  3. MA600A 磁编码器:SPI 接口 12bit 绝对角度采集,实时反馈转子位置;
  4. 28mm 外转子云台无刷电机:330KV、相电阻 6.5Ω、4mm 通孔轴、双滚珠轴承;
  5. 7V 直流稳压电源:系统母线供电;

2. 系统框图**(基于豆包AI)**

设计思路

整体采用分层模块化硬件架构,分为电源供电层、感知采集层、主控运算层、功率驱动执行层、人机交互层五层;软件基于 SimpleFOC 开源 FOC 算法,SPI 读取编码器位置,三环 PID 闭环控制电机,串口实现数据交互与参数配置,全程硬件电流采样做过载保护。

软硬件介绍

  • 硬件:模块化分离设计,编码器 SPI 独立硬件总线,驱动 PWM、电流采样、故障检测独立 IO,无引脚冲突;电源增加多级滤波抑制 PWM 干扰编码器。
  • 软件:分编码器读取子程序、FOC 控制主循环、串口交互子程序、故障保护子程序四大模块,可单独剥离编码器程序单独验证角度采集功能。

3. 各部分功能说明

3.1 ESP32-S3 主控模块 ESP32-S3 Wi-Fi & BLE 5 SoC|乐鑫科技

功能:系统核心运算中枢,分配硬件 HSPI 总线读取 MA600A 角度;输出三路 PWM 波形给 MP6543;运行角度 / 速度 / 电流三环 PID;串口实时输出角度、三相电流调试数据;监听串口数字指令修改电机目标定位角度;采集 MP6543 故障引脚实现过流过热停机保护。 配图:ESP32 排针引脚接线标注图(IO4/5/6/7/9/10/14/18/47 对应外设)

3.2 MA600A 磁编码器采集模块

MA600A INL<0.6° (<0.1°)、高准确度、高带宽、可配置数字磁角度传感器


功能:TMR 隧道磁阻传感器,12bit 绝对角度采集,通过 SPI 总线向 ESP32 实时传输转子机械角度;配套独立测试程序可单独验证角度读数,解决旋转角度跳变、字节乱跳等通讯问题;硬件上同轴安装在电机通孔轴顶端,保证采集精度。

3.3 MP6543 三相驱动模块

MP6543-支持 ENBL 和 PWM 输入的 12V/2A 3 相功率级


功能:接收 ESP32 三路 PWM 信号生成三相正弦驱动电压,驱动无刷电机运转;集成 0.1Ω 采样电阻,硬件采集三相相电流送入 ESP32 ADC;内置过流、过热、欠压保护,故障时拉低 nFAULT 引脚通知主控切断驱动输出;适配 3~12V 母线电压,持续 2A 输出匹配云台电机。

3.4 28mm 云台无刷执行电机

购买链接:28mm钕强磁外转子云台无刷电机 330KV2204三相无刷带限位云台电机


功能:330KV 低转速大扭矩外转子电机,双滚珠轴承转动顺滑,4mm 通孔轴适配磁钢安装;相电阻 6.5Ω 匹配 MP6543 电流输出范围;钕铁硼强磁钢提供稳定磁场,配合 MA600A 实现高精度位置闭环,可作为云台关节、力反馈旋钮执行机构。

3.5 串口人机交互模块

功能:上位机串口监视器双向通讯,实时打印原始 12bit 角度、实时机械角度、三相相电流;支持下发数字指令修改电机目标锁定角度,动态调整电机居中位置,直观调试闭环 PID 参数。

3.6 蓝牙 HID 音量控制模块

功能 :利用 MA600A 磁编码器的高精度角度反馈,将电机轴变成一个"物理旋钮"。当电机自由转动时,ESP32 通过 BLE 发送媒体键 KEY_MEDIA_VOLUME_UP / KEY_MEDIA_VOLUME_DOWN 给电脑,实现音量调节。

4. 作品结果与资源

PCBA硬件资料

力反馈旋钮PCBA(嘉立创&AD文件).zip (218.2 KB)
mp6543的力反馈旋钮BOM清单&嘉立创.zip (8.2 KB)


源码说明

  1. 硬件自检程序:对 MP6543 三相无刷电机驱动器和 MA600A 磁编码器进行硬件级诊断
    包含: 编码器通信测试、电流采样测试、PWM输出测试、开环转动测试

硬件&软件完整说明.pdf (320.2 KB)
源代码&添加每行注释:Hardware_Diagnostic.zip (12.3 KB)

/**
 * MP6543 + MA600A 硬件诊断程序 (适配 ESP32 Arduino Core 3.0+)
 * 
 * 功能: 对 MP6543 三相无刷电机驱动器和 MA600A 磁编码器进行硬件级诊断
 * 包含: 编码器通信测试、电流采样测试、PWM输出测试、开环转动测试
 */

#include <SPI.h>  // 引入 SPI 库,用于与 MA600A 编码器通信

// ==================== 引脚定义 ====================
// 以下引脚根据实际硬件连接定义,ESP32 支持任意 GPIO 映射 PWM/SPI/ADC

// --- MP6543 三相 PWM 输入引脚 ---
// MP6543 是三相半桥驱动器,通过三路 PWM 控制三相电机的 A/B/C 相
#define PWMA_PIN   15   // A 相 PWM 输入 -> 连接 MP6543 的 INA 引脚
#define PWMB_PIN   16   // B 相 PWM 输入 -> 连接 MP6543 的 INB 引脚
#define PWMC_PIN   17   // C 相 PWM 输入 -> 连接 MP6543 的 INC 引脚

// --- MP6543 控制引脚 ---
#define EN_PIN     10   // 驱动器使能引脚 -> 连接 MP6543 的 EN 引脚,高电平使能输出
#define nFAULT_PIN  4   // 故障指示引脚 -> 连接 MP6543 的 nFAULT 引脚,低电平有效(开漏输出,需上拉)

// --- MP6543 电流采样输出引脚 (模拟输入) ---
// SOA/SOB/SOC 分别对应三相电流检测输出,通过运放放大后送入 ESP32 ADC
#define ADC_SOA    33   // A 相电流采样 -> 连接 MP6543 的 SOA 引脚,经运放后接入 ESP32 GPIO33 (ADC1_CH5)
#define ADC_SOB    34   // B 相电流采样 -> 连接 MP6543 的 SOB 引脚,经运放后接入 ESP32 GPIO34 (ADC1_CH6)
#define ADC_SOC    35   // C 相电流采样 -> 连接 MP6543 的 SOC 引脚,经运放后接入 ESP32 GPIO35 (ADC1_CH7)

// --- MA600A 编码器 SPI 接口引脚 ---
// MA600A 是 14bit 磁编码器,通过 SPI 协议输出绝对角度值
#define SPI_SCK    7    // SPI 时钟线 -> 连接 MA600A 的 SCK 引脚
#define SPI_MOSI   8    // SPI 主出从入 -> 连接 MA600A 的 MOSI/SDI 引脚
#define SPI_MISO   9    // SPI 主入从出 -> 连接 MA600A 的 MISO/SDO 引脚
#define SPI_CS     6    // SPI 片选线 -> 连接 MA600A 的 CS 引脚,低电平选中芯片

// ==================== LEDC API 兼容性宏 ====================
// ESP32 Arduino Core 3.0+ 对 LEDC API 做了重大变更,此处定义兼容性宏以兼容新旧版本

#if defined(ARDUINO_ESP32_VERSION_MAJOR) && ARDUINO_ESP32_VERSION_MAJOR >= 3
  // 3.0+ 版本: ledcAttachChannel 合并了 ledcSetup + ledcAttachPin 的功能
  // 参数: (引脚, 频率, 分辨率, 通道号)
  #define PWM_INIT_CH(pin, freq, res, ch)  ledcAttachChannel((pin), (freq), (res), (ch))
  // 3.0+ 版本: ledcWriteChannel 替代了旧的 ledcWrite,参数为通道号而非引脚号
  #define PWM_WRITE(ch, val)               ledcWriteChannel((ch), (val))
#else
  // 2.x 版本: 需要分两步配置 PWM
  // 第一步 ledcSetup 设置通道参数,第二步 ledcAttachPin 将通道绑定到引脚
  #define PWM_INIT_CH(pin, freq, res, ch)  do { ledcSetup((ch), (freq), (res)); ledcAttachPin((pin), (ch)); } while(0)
  // 2.x 版本: ledcWrite 通过引脚号写入,而非通道号
  #define PWM_WRITE(ch, val)               ledcWrite((ch), (val))
#endif

// ==================== 全局变量 ====================
SPIClass *hspi = NULL;  // HSPI 总线指针,ESP32 有两组 SPI: VSPI(默认) 和 HSPI,此处使用 HSPI 避免与 Flash 冲突

/**
 * 向 MA600A 编码器发送命令并读取返回数据
 * @param cmd  发送的 16bit 命令字 (通常为 0x0000 读取角度)
 * @return     编码器返回的 16bit 数据 (高 2bit 为状态,低 14bit 为角度值)
 * 
 * 通信协议: SPI Mode3 (CPOL=1, CPHA=1), MSB 优先, 最大时钟 8MHz
 * 时序: CS 拉低 -> 等待 2us -> 16 时钟周期传输 -> CS 拉高 -> 等待 2us
 */
uint16_t ma600a_read(uint16_t cmd) {
    hspi->beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE3));  // 设置 SPI 参数: 8MHz, MSB优先, Mode3
    digitalWrite(SPI_CS, LOW);   // 片选拉低,选中 MA600A 芯片
    delayMicroseconds(2);        // 等待芯片响应建立时间 (t1 > 1.5us per datasheet)
    uint16_t result = hspi->transfer16(cmd);  // 全双工传输: 发送 cmd 的同时接收 16bit 数据
    digitalWrite(SPI_CS, HIGH);  // 片选拉高,释放芯片
    hspi->endTransaction();      // 结束 SPI 事务,释放总线
    delayMicroseconds(2);        // 等待芯片恢复时间
    return result;
}

/**
 * 将 ADC 原始值转换为电压值
 * @param adc  12bit ADC 原始读数 (0~4095)
 * @return     对应的电压值 (0~3.3V)
 * 
 * ESP32 的 ADC 为 12bit 分辨率,参考电压 3.3V
 * 公式: V = adc * 3.3 / 4095
 */
float adcToVoltage(int adc) {
    return adc * 3.3f / 4095.0f;
}

// ==================== 测试1: MA600A编码器 ====================
/**
 * 测试 MA600A 磁编码器的 SPI 通信和角度读取功能
 * 步骤:
 *   1. 初始化 HSPI 总线和片选引脚
 *   2. 连续读取两次角度值,检查通信稳定性
 *   3. 让用户手动转动电机轴,观察角度变化是否连续
 * 
 * 接线检查要点:
 *   - CS 引脚必须有上拉电阻 (4.7K~10K),防止上电时误触发
 *   - SCK/MOSI/MISO 走线尽量短,避免信号干扰
 *   - 编码器与电机轴的磁铁距离应在 0.5~2mm 范围内
 */
void testMA600A() {
    Serial.println("\n========== 测试1: MA600A 编码器 ==========");

    // 创建 HSPI 实例并初始化
    // HSPI 使用 SPI_SCK/SPI_MISO/SPI_MOSI 作为时钟/主入/主出,SPI_CS 作为片选
    hspi = new SPIClass(HSPI);
    hspi->begin(SPI_SCK, SPI_MISO, SPI_MOSI, SPI_CS);

    // 配置片选引脚为输出模式,默认拉高 (未选中状态)
    pinMode(SPI_CS, OUTPUT);
    digitalWrite(SPI_CS, HIGH);

    // 第一次读取: 用于唤醒编码器并获取初始值
    uint16_t raw1 = ma600a_read(0x0000);
    delay(10);  // 等待 10ms 让编码器稳定

    // 第二次读取: 验证通信一致性
    uint16_t raw2 = ma600a_read(0x0000);

    // 打印两次读取的原始值,用于对比
    Serial.printf("第一次读取: %d (0x%04X)\n", raw1, raw1);
    Serial.printf("第二次读取: %d (0x%04X)\n", raw2, raw2);

    // 判断读取值是否异常
    // 0x0000 或 0xFFFF 通常表示 SPI 通信失败或接线错误
    if (raw1 == 0x0000 || raw1 == 0xFFFF) {
        Serial.println("[警告] 读取值异常,请检查接线!");
        Serial.println("       可能原因: CS 未上拉 / SPI 线序错误 / 编码器未供电");
    } else {
        Serial.println("[通过] MA600A 通信正常");
    }

    // 连续 20 次读取,观察角度变化
    // 用户应手动缓慢转动电机轴,观察角度是否平滑变化
    Serial.println("\n请手动缓慢转动电机轴,观察角度变化:");
    for (int i = 0; i < 20; i++) {
        uint16_t raw = ma600a_read(0x0000);  // 发送 0x0000 读取角度寄存器
        // MA600A 返回 16bit 数据: 高 2bit 为状态/奇偶校验,低 14bit 为角度
        // 角度范围 0~16383 (14bit),对应 0°~360°
        float deg = raw * 360.0f / 65536.0f;  // 将 16bit 值映射到 0~360° (实际精度为 14bit)
        Serial.printf("  角度: %6.2f°  Raw: %5d\n", deg, raw);
        delay(200);  // 每 200ms 读取一次,共 4 秒
    }
}

// ==================== 测试2: MP6543电流采样 ====================
/**
 * 测试 MP6543 的电流采样功能和故障检测
 * 步骤:
 *   1. 配置 ADC 分辨率和衰减系数
 *   2. 未使能驱动器时读取零点 (应接近 1.65V,即运放偏置电压)
 *   3. 使能驱动器后再次读取,确认无异常漂移
 *   4. 检查 nFAULT 引脚状态
 * 
 * 电流采样原理:
 *   MP6543 内置下桥臂电流检测电阻,输出为差分信号
 *   经外部运放放大后,输出为单端信号: Vout = Vref + I * Rsense * Gain
 *   零点 (无电流时) 应接近 Vref (通常为 1.65V = 3.3V/2)
 */
void testCurrentSense() {
    Serial.println("\n========== 测试2: MP6543 电流采样 ==========");

    // 配置 ESP32 ADC 参数
    analogReadResolution(12);        // 设置 ADC 分辨率为 12bit (0~4095)
    analogSetAttenuation(ADC_11db);  // 设置衰减系数为 11dB,量程 0~3.3V (全量程)

    // 阶段1: 驱动器未使能时的零点读取
    // 此时三相半桥关闭,电流采样输出应为运放偏置电压 (~1.65V)
    Serial.println("MP6543 未使能,读取零点:");
    for (int i = 0; i < 5; i++) {
        // 读取三相电流采样引脚的 ADC 值
        int a = analogRead(ADC_SOA);  // A 相电流
        int b = analogRead(ADC_SOB);  // B 相电流
        int c = analogRead(ADC_SOC);  // C 相电流

        // 打印原始值和转换后的电压值
        Serial.printf("  SOA=%4d(%.3fV) SOB=%4d(%.3fV) SOC=%4d(%.3fV)\n",
                      a, adcToVoltage(a), b, adcToVoltage(b), c, adcToVoltage(c));
        delay(100);
    }

    // 阶段2: 使能驱动器后的零点读取
    // 使能后三相半桥处于待机状态,仍不应有电流,零点应保持稳定
    Serial.println("\n使能 MP6543 (EN=HIGH)...");
    pinMode(EN_PIN, OUTPUT);     // 配置 EN 引脚为输出
    digitalWrite(EN_PIN, HIGH);  // 拉高使能驱动器
    delay(100);                  // 等待驱动器稳定

    Serial.println("MP6543 已使能,读取零点:");
    for (int i = 0; i < 5; i++) {
        int a = analogRead(ADC_SOA);
        int b = analogRead(ADC_SOB);
        int c = analogRead(ADC_SOC);
        Serial.printf("  SOA=%4d(%.3fV) SOB=%4d(%.3fV) SOC=%4d(%.3fV)\n",
                      a, adcToVoltage(a), b, adcToVoltage(b), c, adcToVoltage(c));
        delay(100);
    }

    // 阶段3: 故障引脚检测
    // nFAULT 为开漏输出,需外部上拉,正常时高电平,故障时低电平
    pinMode(nFAULT_PIN, INPUT_PULLUP);  // 配置为输入并启用内部上拉
    bool fault = !digitalRead(nFAULT_PIN);  // 读取并取反: 低电平表示故障

    if (fault) {
        Serial.println("[警告] nFAULT 为低电平,MP6543 报告故障!");
        Serial.println("       可能原因: 过流保护 / 过温保护 / 欠压锁定");
    } else {
        Serial.println("[通过] nFAULT 正常 (高电平)");
    }

    // 测试结束,关闭驱动器
    digitalWrite(EN_PIN, LOW);
}

// ==================== 测试3: PWM输出 ====================
/**
 * 测试三路 PWM 输出到 MP6543 的 A/B/C 相
 * 步骤:
 *   1. 初始化三个 LEDC 通道,频率 20kHz,分辨率 10bit (0~1023)
 *   2. 使能驱动器
 *   3. 依次给 A/B/C 相输出 30% 占空比 PWM
 *   4. 用户可用示波器或万用表测量对应相的输出
 * 
 * 安全提示:
 *   - 此测试仅输出 PWM 信号,不形成旋转磁场,电机不会转动
 *   - 但三相半桥会交替导通,电机绕组会有电流,可能发热
 *   - 建议断开电机或确保电机轴可自由转动
 */
void testPWM() {
    Serial.println("\n========== 测试3: PWM 输出测试 ==========");

    // 初始化三个 PWM 通道
    // 参数: (引脚, 频率, 分辨率, 通道号)
    // 通道号 0/1/2 分别对应 A/B/C 相
    PWM_INIT_CH(PWMA_PIN, 20000, 10, 0);  // A 相: 20kHz, 10bit, 通道0
    PWM_INIT_CH(PWMB_PIN, 20000, 10, 1);  // B 相: 20kHz, 10bit, 通道1
    PWM_INIT_CH(PWMC_PIN, 20000, 10, 2);  // C 相: 20kHz, 10bit, 通道2

    // 使能驱动器
    pinMode(EN_PIN, OUTPUT);
    digitalWrite(EN_PIN, HIGH);

    Serial.println("依次输出 PWM 到三相 (占空比 30%)");
    // 10bit 分辨率下,30% 占空比 = 1023 * 0.3 ≈ 307

    // A 相输出 30% PWM,B/C 相关闭
    Serial.println("\nA相输出...");
    PWM_WRITE(0, 307);  // 通道0 (A相) = 307 (30%)
    PWM_WRITE(1, 0);    // 通道1 (B相) = 0 (0%)
    PWM_WRITE(2, 0);    // 通道2 (C相) = 0 (0%)
    delay(1000);        // 持续 1 秒,方便测量

    // B 相输出 30% PWM,A/C 相关闭
    Serial.println("B相输出...");
    PWM_WRITE(0, 0);
    PWM_WRITE(1, 307);  // 通道1 (B相) = 307 (30%)
    PWM_WRITE(2, 0);
    delay(1000);

    // C 相输出 30% PWM,A/B 相关闭
    Serial.println("C相输出...");
    PWM_WRITE(0, 0);
    PWM_WRITE(1, 0);
    PWM_WRITE(2, 307);  // 通道2 (C相) = 307 (30%)
    delay(1000);

    // 关闭所有 PWM 输出并禁用驱动器
    PWM_WRITE(0, 0);
    PWM_WRITE(1, 0);
    PWM_WRITE(2, 0);
    digitalWrite(EN_PIN, LOW);

    Serial.println("\n[通过] PWM 测试完成");
}

// ==================== 测试4: 开环转动 ====================
/**
 * 开环六步换相测试,验证电机和驱动器是否能正常转动
 * 步骤:
 *   1. 初始化 PWM 通道
 *   2. 使能驱动器
 *   3. 按六步换相顺序依次导通两相,形成旋转磁场
 *   4. 观察电机是否按预期方向转动
 * 
 * 六步换相顺序 (两两导通):
 *   Step0: A+B-  (A相上桥导通,B相下桥导通)
 *   Step1: A+C-  
 *   Step2: B+C-
 *   Step3: B+A-
 *   Step4: C+A-
 *   Step5: C+B-
 * 
 * 安全警告:
 *   - 此测试会使电机转动!确保电机轴自由无负载!
 *   - 开环无位置反馈,换相速度固定,若负载过大可能失步
 *   - 若电机不转但有嗡嗡声,可能是相序错误或霍尔信号不匹配
 */
void testOpenLoop() {
    Serial.println("\n========== 测试4: 开环转动测试 ==========");
    Serial.println("[警告] 电机将会转动!确保轴自由无负载!");
    Serial.println("5秒后开始...");
    delay(5000);  // 给用户 5 秒时间确认安全

    // 使能驱动器
    pinMode(EN_PIN, OUTPUT);
    digitalWrite(EN_PIN, HIGH);

    // 初始化 PWM 通道
    PWM_INIT_CH(PWMA_PIN, 20000, 10, 0);
    PWM_INIT_CH(PWMB_PIN, 20000, 10, 1);
    PWM_INIT_CH(PWMC_PIN, 20000, 10, 2);

    // PWM 占空比设置
    // 200/1023 ≈ 19.5%,较低的占空比用于安全测试
    const int pwm_val = 200;
    // 每步持续时间 50ms,形成约 3.33Hz 的换相频率
    const int step_delay = 50;

    Serial.println("6步换相开环转动...");
    // 执行 60 步 (10 圈),每圈 6 步
    for (int i = 0; i < 60; i++) {
        int step = i % 6;  // 计算当前处于六步中的哪一步

        // 根据步数设置对应相的 PWM
        // 注意: 此处仅控制上桥臂 PWM,下桥臂由 MP6543 内部逻辑控制
        // 实际两两导通需要配合 INL 引脚,此处简化处理
        switch(step) {
            case 0: PWM_WRITE(0, pwm_val); PWM_WRITE(1, 0); PWM_WRITE(2, 0); break;  // A相导通
            case 1: PWM_WRITE(0, pwm_val); PWM_WRITE(1, pwm_val); PWM_WRITE(2, 0); break;  // A+B
            case 2: PWM_WRITE(0, 0); PWM_WRITE(1, pwm_val); PWM_WRITE(2, 0); break;  // B相导通
            case 3: PWM_WRITE(0, 0); PWM_WRITE(1, pwm_val); PWM_WRITE(2, pwm_val); break;  // B+C
            case 4: PWM_WRITE(0, 0); PWM_WRITE(1, 0); PWM_WRITE(2, pwm_val); break;  // C相导通
            case 5: PWM_WRITE(0, pwm_val); PWM_WRITE(1, 0); PWM_WRITE(2, pwm_val); break;  // C+A
        }
        delay(step_delay);
    }

    // 关闭所有输出并禁用驱动器
    PWM_WRITE(0, 0);
    PWM_WRITE(1, 0);
    PWM_WRITE(2, 0);
    digitalWrite(EN_PIN, LOW);

    Serial.println("\n[通过] 开环转动测试完成");
}

// ==================== Setup ====================
/**
 * 程序初始化入口
 * 功能:
 *   1. 初始化串口通信 (115200 baud)
 *   2. 打印程序信息和 ESP32 Core 版本
 *   3. 显示操作菜单
 */
void setup() {
    Serial.begin(115200);  // 初始化串口,波特率 115200
    delay(2000);           // 等待串口监视器连接 (部分开发板需要)

    // 打印程序标题和版本信息
    Serial.println("\n========================================");
    Serial.println("  MP6543 + MA600A 硬件诊断程序");
    Serial.print("  ESP32 Core: ");

    // 检测并打印 ESP32 Arduino Core 版本号
    #if defined(ARDUINO_ESP32_VERSION_MAJOR)
      Serial.print(ARDUINO_ESP32_VERSION_MAJOR);   // 主版本号 (如 3)
      Serial.print("."); 
      Serial.print(ARDUINO_ESP32_VERSION_MINOR);   // 次版本号 (如 0)
      Serial.print("."); 
      Serial.println(ARDUINO_ESP32_VERSION_PATCH); // 补丁版本号 (如 1)
    #else
      Serial.println("2.x");  // 未定义版本宏则为 2.x 版本
    #endif

    Serial.println("========================================");
    Serial.println("\n发送 '1' '2' '3' '4' 执行对应测试");
    Serial.println("发送 'a' 执行全部测试");
    Serial.println("========================================\n");
}

// ==================== Loop ====================
/**
 * 主循环
 * 功能: 监听串口输入,根据命令字符执行对应的测试函数
 * 命令映射:
 *   '1' -> testMA600A()      编码器测试
 *   '2' -> testCurrentSense() 电流采样测试
 *   '3' -> testPWM()         PWM 输出测试
 *   '4' -> testOpenLoop()    开环转动测试
 *   'a'/'A' -> 依次执行全部四项测试
 */
void loop() {
    // 检查串口是否有数据到达
    if (Serial.available()) {
        char cmd = Serial.read();  // 读取一个字符命令

        // 根据命令执行对应测试
        switch(cmd) {
            case '1': 
                testMA600A();  // 执行编码器测试
                break;
            case '2': 
                testCurrentSense();  // 执行电流采样测试
                break;
            case '3': 
                testPWM();  // 执行 PWM 输出测试
                break;
            case '4': 
                testOpenLoop();  // 执行开环转动测试
                break;
            case 'a': 
            case 'A':
                // 依次执行全部四项测试
                testMA600A(); 
                testCurrentSense(); 
                testPWM(); 
                testOpenLoop(); 
                break;
            default:
                // 未知命令提示
                Serial.println("未知命令,发送 1/2/3/4/a"); 
                break;
        }
    }
    delay(10);  // 短暂延时,降低 CPU 占用
}

2. 完整 FOC 闭环主程序:电机角度闭环、电流采样、故障保护、串口交互完整功能;

力反馈旋钮硬件&软件完整说明.pdf (381.7 KB)
力反馈旋钮最终程序.zip (402.4 KB)

文件 说明
MP6543_MA600A_ESP32_FOC_BLE_VolumeKnob_Commented.ino 主程序:FOC 控制 + 蓝牙音量旋钮,每行带中文注释
MA600ASensor_Commented.h 编码器驱动头文件,含类定义、宏、配置结构体注释
MA600ASensor_Commented.cpp 编码器驱动实现,含 SPI 通信、寄存器操作注释
MP6543CurrentSense_Commented.h 电流采样头文件,含 ADC 参数、运放接线注释
MP6543CurrentSense_Commented.cpp 电流采样实现,含零点校准、噪声抑制注释
/**
 * MP6543 + MA600A + ESP32 FOC 控制 (蓝牙音量旋钮版)
 * 
 * 新增功能: ESP32 BLE HID 键盘 -> 电脑音量控制
 * 原理: 电机作为物理旋钮,转动时发送蓝牙媒体键(Volume Up/Down)
 * 
 * 依赖库:
 *   - SimpleFOC
 *   - ESP32-BLE-Keyboard (by T-vK)
 *   - MA600ASensor (自定义)
 *   - MP6543CurrentSense (自定义)
 */

#include <SimpleFOC.h>
#include <BleKeyboard.h>
#include "MA600ASensor.h"
#include "MP6543CurrentSense.h"

// ===================== 引脚定义 =====================
#define PWMA_PIN   15
#define PWMB_PIN   16
#define PWMC_PIN   17
#define EN_PIN     10
#define nFAULT_PIN  4

#define ADC_SOA   7
#define ADC_SOB   5
#define ADC_SOC   6

#define SPI_SCK   14
#define SPI_MOSI  18
#define SPI_MISO  9
#define SPI_CS    47

#define POLE_PAIRS      7
#define SUPPLY_VOLTAGE  12.0f

// ===================== 蓝牙键盘实例 =====================
// 设备名称会显示在电脑的蓝牙设备列表中
BleKeyboard bleKeyboard("FOC Volume Knob", "SimpleFOC", 100);

// ===================== FOC 组件 =====================
MA600ASensor sensor = MA600ASensor(SPI_SCK, SPI_MISO, SPI_MOSI, SPI_CS);
BLDCDriver3PWM driver = BLDCDriver3PWM(PWMA_PIN, PWMB_PIN, PWMC_PIN, EN_PIN);
BLDCMotor motor = BLDCMotor(POLE_PAIRS, 6.5f);
MP6543CurrentSense currentSense = MP6543CurrentSense();

Commander command = Commander(Serial);

// ===================== 音量控制状态变量 =====================
// 旋钮模式专用变量
float lastKnobAngle = 0.0f;           // 上一次的角度值 (rad)
float accumulatedDelta = 0.0f;        // 累积的角度变化量
float angleThreshold = 0.15f;         // 触发一次音量键的角度阈值 (~8.6°)
                                      // 可根据手感调整: 越小越灵敏,越大越迟钝

unsigned long lastVolumeSend = 0;     // 上次发送音量键的时间
const unsigned long VOLUME_COOLDOWN = 80;  // 音量键发送最小间隔(ms),防止过快

bool knobModeActive = false;          // 是否处于旋钮模式
bool bleConnected = false;            // 蓝牙连接状态

// ===================== 命令回调 =====================
void doTarget(char* cmd) { command.scalar(&motor.target, cmd); }
void doLimit(char* cmd)  { command.scalar(&motor.voltage_limit, cmd); }
void doP(char* cmd)      { command.scalar(&motor.PID_velocity.P, cmd); }
void doI(char* cmd)      { command.scalar(&motor.PID_velocity.I, cmd); }
void doD(char* cmd)      { command.scalar(&motor.PID_velocity.D, cmd); }
void doTf(char* cmd)     { command.scalar(&motor.LPF_velocity.Tf, cmd); }

// 控制模式切换
void doMode(char* cmd) {
    int mode = atoi(cmd);
    switch(mode) {
        case 0: motor.controller = MotionControlType::torque; 
                Serial.println("模式: Torque"); break;
        case 1: motor.controller = MotionControlType::velocity; 
                Serial.println("模式: Velocity (闭环速度)"); break;
        case 2: motor.controller = MotionControlType::angle; 
                Serial.println("模式: Angle (闭环位置)"); break;
        case 3: motor.controller = MotionControlType::velocity_openloop; 
                Serial.println("模式: Velocity Openloop (开环速度)"); break;
        case 4: motor.controller = MotionControlType::angle_openloop; 
                Serial.println("模式: Angle Openloop (开环位置)"); break;
        default: Serial.println("无效模式 (0-4)"); return;
    }
    knobModeActive = false;  // 切换其他模式时退出旋钮模式
}

// 旋钮模式开关
void doKnobMode(char* cmd) {
    int en = atoi(cmd);
    if (en == 1) {
        knobModeActive = true;
        motor.controller = MotionControlType::torque;  // 力矩模式,自由转动
        motor.target = 0;                              // 目标力矩为0,无阻力
        lastKnobAngle = sensor.getSensorAngle();       // 记录当前角度作为基准
        accumulatedDelta = 0;
        Serial.println("[旋钮模式] 已激活!转动电机控制电脑音量");
        Serial.println("        顺时针: 音量+ | 逆时针: 音量-");
    } else {
        knobModeActive = false;
        Serial.println("[旋钮模式] 已关闭");
    }
}

// 调整旋钮灵敏度 (角度阈值)
void doSensitivity(char* cmd) {
    float val = atof(cmd);
    if (val > 0.01f && val < 1.0f) {
        angleThreshold = val;
        Serial.printf("[旋钮] 灵敏度阈值设置为: %.3f rad (%.1f°)\n", 
                      val, degrees(val));
    } else {
        Serial.println("[错误] 阈值范围: 0.01 ~ 1.0 rad");
    }
}

// ===================== 故障中断 =====================
volatile bool faultTriggered = false;
void IRAM_ATTR onFault() { faultTriggered = true; }

// ===================== 音量控制核心逻辑 =====================
/**
 * 处理旋钮转动 -> 发送蓝牙音量键
 * 
 * 算法说明:
 *   1. 读取当前编码器角度
 *   2. 计算与上次记录角度的差值 (处理角度跳变,如从 2π 到 0)
 *   3. 累积角度变化量
 *   4. 当累积量超过阈值时,发送对应的音量键
 *   5. 扣除已触发的累积量,保留余量
 */
void handleVolumeKnob() {
    if (!knobModeActive) return;
    if (!bleKeyboard.isConnected()) return;

    float currentAngle = sensor.getSensorAngle();

    // 计算角度差值,处理 2π 跳变
    float delta = currentAngle - lastKnobAngle;

    // 处理角度从 2π -> 0 或 0 -> 2π 的跳变
    if (delta > PI) {
        delta -= TWO_PI;   // 逆时针跨过0点
    } else if (delta < -PI) {
        delta += TWO_PI;   // 顺时针跨过0点
    }

    lastKnobAngle = currentAngle;
    accumulatedDelta += delta;

    unsigned long now = millis();
    if (now - lastVolumeSend < VOLUME_COOLDOWN) return;

    // 检查累积量是否超过阈值
    if (fabs(accumulatedDelta) >= angleThreshold) {
        int steps = (int)(fabs(accumulatedDelta) / angleThreshold);

        // 限制单次最大发送次数,防止爆发
        if (steps > 3) steps = 3;

        if (accumulatedDelta > 0) {
            // 顺时针 -> 音量增加
            for (int i = 0; i < steps; i++) {
                bleKeyboard.write(KEY_MEDIA_VOLUME_UP);
            }
            Serial.printf("[音量+] 发送 %d 次 (累积: %.3f rad)\n", 
                          steps, accumulatedDelta);
        } else {
            // 逆时针 -> 音量减少
            for (int i = 0; i < steps; i++) {
                bleKeyboard.write(KEY_MEDIA_VOLUME_DOWN);
            }
            Serial.printf("[音量-] 发送 %d 次 (累积: %.3f rad)\n", 
                          steps, accumulatedDelta);
        }

        // 扣除已处理的角度
        float consumed = (accumulatedDelta > 0 ? 1.0f : -1.0f) 
                         * steps * angleThreshold;
        accumulatedDelta -= consumed;

        lastVolumeSend = now;
    }
}

// ===================== 蓝牙状态检测 =====================
void checkBleConnection() {
    static bool wasConnected = false;
    bool isConnected = bleKeyboard.isConnected();

    if (isConnected && !wasConnected) {
        Serial.println("[蓝牙] 已连接到电脑!");
        bleConnected = true;
    } else if (!isConnected && wasConnected) {
        Serial.println("[蓝牙] 连接已断开");
        bleConnected = false;
    }
    wasConnected = isConnected;
}

// ===================== Setup =====================
void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n========================================");
    Serial.println("  FOC 控制 + 蓝牙音量旋钮");
    Serial.println("========================================");

    // ---- 蓝牙键盘初始化 ----
    Serial.println("[BLE] 启动蓝牙键盘...");
    bleKeyboard.begin();
    Serial.println("[BLE] 蓝牙广播中,请在电脑蓝牙设置中配对 'FOC Volume Knob'");

    // ---- 硬件初始化 ----
    pinMode(nFAULT_PIN, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(nFAULT_PIN), onFault, FALLING);

    sensor.init();
    Serial.printf("MA600A: PRT=0x%02X Angle=%.2f°\n", 
                  sensor.readRegister(0x00), degrees(sensor.getSensorAngle()));

    currentSense.init();

    driver.voltage_power_supply = SUPPLY_VOLTAGE;
    driver.voltage_limit = 9.0f;
    driver.pwm_frequency = 20000;
    driver.init();

    motor.linkSensor(&sensor);
    motor.linkDriver(&driver);

    // 初始模式: 角度控制 (已知正常)
    motor.controller = MotionControlType::angle;
    motor.torque_controller = TorqueControlType::voltage;

    // 速度环 PID (M1抖动修复)
    motor.PID_velocity.P = 0.02f;
    motor.PID_velocity.I = 2.0f;
    motor.PID_velocity.D = 0.0f;
    motor.PID_velocity.output_ramp = 1000.0f;
    motor.PID_velocity.limit = 3.0f;

    // 速度低通滤波
    motor.LPF_velocity.Tf = 0.05f;

    // 位置环
    motor.P_angle.P = 15.0f;
    motor.P_angle.limit = 15.0f;

    // 限制
    motor.velocity_limit = 50.0f;
    motor.voltage_limit = 6.0f;
    motor.current_limit = 0.6f;

    // 对齐参数
    motor.voltage_sensor_align = 3.0f;

    motor.useMonitoring(Serial);
    motor.monitor_downsample = 100;

    motor.init();
    Serial.println("[...] FOC 初始化...");
    motor.initFOC();
    Serial.println("[OK] FOC 初始化完成!");

    // ---- 命令注册 ----
    command.add('T', doTarget, "目标值");
    command.add('L', doLimit,  "电压限制");
    command.add('P', doP,      "速度P (默认0.02)");
    command.add('I', doI,      "速度I (默认2)");
    command.add('D', doD,      "速度D");
    command.add('F', doTf,     "速度滤波Tf (默认0.05)");
    command.add('M', doMode,   "模式(0=torque 1=velocity 2=angle 3=vel_open 4=angle_open)");
    command.add('K', doKnobMode, "旋钮模式 (1=开 0=关)");
    command.add('S', doSensitivity, "旋钮灵敏度阈值 rad (默认0.15)");

    // ---- 使用指南 ----
    Serial.println("\n========== 使用指南 ==========");
    Serial.println("【FOC控制】");
    Serial.println("  M2(angle)正常: 发送 T3.14 测试");
    Serial.println("  M1(velocity):  发送 M1 -> T10");
    Serial.println("  M3(vel_open):  发送 L9 -> M3 -> T5");
    Serial.println("");
    Serial.println("【蓝牙音量旋钮】");
    Serial.println("  1. 在电脑蓝牙设置中配对 'FOC Volume Knob'");
    Serial.println("  2. 发送 K1 激活旋钮模式");
    Serial.println("  3. 转动电机: 顺时针=音量+  逆时针=音量-");
    Serial.println("  4. 发送 S0.1 调灵敏度 (越小越灵敏)");
    Serial.println("  5. 发送 K0 退出旋钮模式");
    Serial.println("==============================\n");
}

// ===================== Loop =====================
void loop() {
    // 故障检测
    if (faultTriggered) {
        Serial.println("[FAULT] MP6543保护!");
        motor.disable();
        faultTriggered = false;
        delay(2000);
        motor.enable();
    }

    // FOC 主循环
    motor.loopFOC();
    motor.move();

    // 蓝牙状态检测
    checkBleConnection();

    // 旋钮音量控制 (在力矩模式下,让电机自由转动)
    if (knobModeActive) {
        // 力矩模式,目标为0,让电机可以自由转动
        motor.target = 0;
        handleVolumeKnob();
    }

    // 串口命令
    command.run();
    motor.monitor();
}



** 致谢:**

首先感谢MPS提供的这次参赛机会,让我感受到新一代芯片的强大功能,其中mp6543仅仅一颗芯片,完成了无刷电机控制,电流反馈等等功能,让电路设计如此简单,就像MPS的宣传语:“我们让电源设计工作变得妙趣横生!”

科技不断进步,学习永无止境。

此产品使用以下AI及开源资料用于产品的设计,特此致谢:

1、豆包AI (完成内部图片的生成,代码注释,文字优化等)

部分截图:

2、kimi(完成内部图片的生成,代码注释,文字优化等)

部分截图:

3、灯哥FOC控制(力反馈旋钮的资源)

DengFOC官方文档

4、开源代码资源:

ESP32-蓝牙电脑代码:GitHub-ESP32-BLE-Keyboard