ESP32+MP6543+MA600A 28mm 云台无刷 FOC 力反馈旋钮
1. 作品简介
(1)基础信息
作品名称:基于 SimpleFOC 的云台无刷电机力反馈闭环控制器
作 者:MetalHeart
作品照片:
(2)作品功能介绍
- 本作品以 ESP32 为主控,采用 MP6543 三相无刷驱动、MA600A TMR 磁编码器搭建完整 FOC 磁场定向闭环控制系统。可实现电机角度闭环锁定、串口实时读取转子绝对角度、串口指令修改目标定位角度;内置硬件过流、过热故障检测保护;支持 360° 全量程高精度角度采集,适用于小型云台、力反馈旋钮、小型关节伺服等场景,解决传统开环电机抖动、定位精度差问题。
- ESP32 开启 BLE 蓝牙 HID 多媒体键盘模式,电脑蓝牙配对设备后,实时读取 MA600A 输出的电机角度变化差值;电机正向旋转发送音量增大多媒体按键指令,反向旋转发送音量减小指令,实现旋钮式音量调节。
(3)物料清单及简介
- ESP32-S3 主控:核心运算单元,运行 SimpleFOC 算法、SPI 读取编码器、串口交互;
- MP6543 三相无刷驱动芯片:集成低侧电流采样,驱动 28mm 云台无刷电机;
- MA600A 磁编码器:SPI 接口 12bit 绝对角度采集,实时反馈转子位置;
- 28mm 外转子云台无刷电机:330KV、相电阻 6.5Ω、4mm 通孔轴、双滚珠轴承;
- 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)
源码说明
- 硬件自检程序:对 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();
}












