
1. 为什么我选择用ESP32做无刷电机FOC控制1.1 从一次风扇改造说起去年夏天我把家里一台老式落地扇拆了原因很简单——那台风扇只有三档机械调速晚上睡觉要么太吵要么不够凉而且交流电机低频嗡嗡声实在让人难受。当时我脑子里冒出的第一个念头就是能不能换成无刷电机用FOC做无级调速再挂个温度传感器做自动控温这个想法落地之后就有了今天这篇东西。无刷电机加FOCField-Oriented Control磁场定向控制这套组合在工业伺服和高端云台里已经非常成熟但放到DIY场景里很多人第一反应是这玩意儿门槛高。确实如果从零写FOC算法光是Clarke变换、Park变换、SVPWM、电流采样时序、转子位置估算这一套下来没个把月啃不下来。但SimpleFOC这个开源库把大部分脏活累活都封装好了配合ESP32这种双核240MHz、带硬件浮点、自带WiFi和蓝牙的芯片做一台智能风扇的难度被拉到了周末两天能搞定的水平。这篇文章适合三类人一是手里有ESP32和无刷电机、想入门FOC但不知道从哪下手的嵌入式爱好者二是想做一个带温控、带App控制的智能风扇的DIY玩家三是已经用过SimpleFOC但调参总是调不好的朋友。我会把选型逻辑、接线、代码、调参、踩坑全部摊开讲代码可以直接抄。1.2 ESP32相比STM32做FOC的取舍网上搜无刷电机FOC出来的大量是STM32方案比如STM32F103RCT6、STM32F405这些。STM32做FOC确实是经典路线定时器资源丰富、互补PWM带死区控制、ADC注入通道采样时机精准ST的MCSDK甚至能一键生成FOC工程。但我最终选ESP32理由有这么几条。第一是集成度。智能风扇需要联网、需要蓝牙配网、需要接温度传感器STM32要额外挂WiFi模块或者蓝牙模块PCB面积和成本都上去了。ESP32一颗芯片全包WiFi和蓝牙可以同时工作这一点后面会细说省掉一堆外围。第二是算力够用。ESP32双核Xtensa LX6主频240MHz带单精度硬件浮点。FOC的运算量主要是三角函数和PIDSimpleFOC在ESP32上跑10kHz的电流环毫无压力速度环1kHz更是绰绰有余。相比之下STM32F103没有硬件浮点跑浮点FOC会比较吃力得用定点或者查表优化。第三是开发体验。Arduino框架下SimpleFOC的API极其简洁几行代码就能让电机转起来。当然代价是实时性不如裸机STM32那么确定但对风扇这种负载变化缓慢的场景完全够用。注意如果你要做的是高速主轴、无人机电调这种对实时性要求极高的场景还是老老实实上STM32或者专用FOC芯片。ESP32的WiFi中断会偶尔打断控制循环虽然SimpleFOC用了独立的定时器任务来缓解但确定性不如STM32。1.3 智能风扇这个项目的核心需求拆解把智能风扇这四个字拆开实际需求是这么几层电机层无刷电机要能平滑调速低速不抖高速不啸叫支持正反转。传感层至少一路温度传感器采集环境温度可选加一路霍尔或者编码器做闭环。控制层根据温度自动调节转速支持手动模式和自动模式切换。交互层手机App或者网页能看状态、调档位、切模式。安全层堵转保护、过流保护、过温保护。这五层里电机层是地基也是最容易翻车的地方。很多人卡在电机不转或者转起来抖得像筛糠问题基本都出在电机层。所以下面我会把大量篇幅放在电机驱动和FOC调参上交互层反而简单ESP32跑个WebSocket服务器就行。2. 硬件选型与接线别在第一步就翻车2.1 核心器件清单与选型理由先把我用的清单列出来再说为什么这么选。器件型号选型理由主控ESP32-WROOM-32双核240MHzWiFiBTGPIO够用便宜好买电机2204云台无刷电机带霍尔体积小、扭矩够、自带霍尔省去无感估算驱动DRV8313或SimpleFOC Shield三相半桥集成带过流保护3.3V逻辑兼容编码器AS5600磁编码器I2C接口12位分辨率比霍尔精度高一个量级温度传感器DS18B20单总线防水封装可选精度0.5度电源12V 3A开关电源电机额定电压匹配留足余量这里重点说两个选型决策。为什么用带霍尔的电机而不是无感FOC无感FOC靠反电动势估算转子位置低速时反电动势很小估算不准启动困难。风扇恰恰需要低速运行晚上微风档所以无感方案在低速段体验很差。带霍尔或者带磁编码器的电机从零速开始就有准确位置启动平顺低速扭矩稳定。我一开始图便宜买了个无感电机结果低速启动要抖三下才能转起来后来换成带霍尔的才解决。为什么加AS5600而不是只用霍尔霍尔的分辨率通常是每电周期6个状态换算到机械角度大概30度一个台阶做速度环够用但做位置环或者要求极低速平滑时会有明显的齿槽感。AS5600是12位绝对角度4096个刻度配合SimpleFOC的MagneticSensorI2C低速平滑度提升非常明显。如果你只做风扇调速霍尔其实够如果想做摆头角度控制AS5600值得加。2.2 三相驱动接线与电源去耦接线这块我把ESP32、DRV8313、电机三者的连接画成文字版ESP32 GPIO25 -- DRV8313 IN1 (U相) ESP32 GPIO26 -- DRV8313 IN2 (V相) ESP32 GPIO27 -- DRV8313 IN3 (W相) ESP32 GPIO14 -- DRV8313 EN (使能) DRV8313 OUT1/2/3 -- 电机三相线 DRV8313 VM -- 12V电源正 DRV8313 GND -- 12V电源负 ESP32 GND必须共地霍尔接线电机霍尔H1 -- ESP32 GPIO34 电机霍尔H2 -- ESP32 GPIO35 电机霍尔H3 -- ESP32 GPIO32 霍尔VCC -- 3.3V 霍尔GND -- GND注意霍尔传感器有的是开漏输出需要上拉电阻通常4.7k到10k。我用的电机霍尔是集电极开路一开始没加上拉读出来全是0查了半天才发现。如果你读霍尔一直是固定值先检查上拉。电源去耦是另一个高频翻车点。无刷电机换向瞬间电流突变很大如果电源走线细、去耦电容不够会导致ESP32复位或者驱动芯片误触发。我的做法是驱动板VM和GND之间并一个470uF电解电容加一个0.1uF陶瓷电容电解电容吸收低频波动陶瓷电容滤高频。ESP32的3.3V和GND之间并一个10uF加0.1uF。电机三相线尽量短如果必须延长用三根等长的线避免引入不对称电感。我踩过的坑第一次做的时候只焊了个100uF电容电机一加速ESP32就重启示波器一看VM上有个2V左右的尖峰。换成470uF电解加0.1uF陶瓷之后尖峰压到300mV以内再没重启过。2.3 ESP32引脚分配与避坑ESP32有些引脚是敏感引脚用之前必须知道GPIO6到GPIO11连接内部SPI Flash绝对不能用。GPIO34到GPIO39只能做输入不能做输出没有内部上拉。霍尔输入用这些没问题但要注意外部上拉。GPIO0启动模式选择上电时拉低会进下载模式做输出要小心。GPIO2启动时如果拉高可能影响下载做普通输出问题不大。ADC2通道WiFi开启时ADC2不可用如果要做电流采样用ADC1GPIO32-39。我的分配方案是PWM用GPIO25/26/27都在ADC2但做PWM没问题霍尔用GPIO34/35/32I2C用GPIO21/22DS18B20用GPIO4使能用GPIO14。这套分配避开了所有敏感引脚实测稳定。3. SimpleFOC库核心原理与代码实现3.1 FOC到底在干什么一个生活化类比很多人被FOC的数学公式吓退其实核心思想可以用一个类比讲清楚。想象你在推一个旋转木马。如果你只是随便找个方向使劲推木马转起来会一顿一顿的效率低。FOC的做法是先实时测量木马当前转到哪个角度然后你始终站在最省力的那个切向位置去推而且推力大小可以精确控制。这样木马转得又稳又省力。对应到电机定子三相电流合成一个磁场矢量转子永磁体也有自己的磁场。FOC就是实时测量转子角度然后控制定子电流矢量始终垂直于转子磁场方向这样产生的转矩最大而平行方向的分量不产生转矩、只发热被压到零。这就是所谓的磁场定向。实现上分几步采样三相电流用Clarke变换把三相变两相αβ再用Park变换把静止坐标系转到随转子旋转的dq坐标系d轴分量控制到0q轴分量控制转矩。然后PID调节再反变换回三相用SVPWM输出。SimpleFOC把这些全封装了你只需要配置参数。3.2 SimpleFOC的三种控制模式SimpleFOC支持三种闭环模式风扇项目里我都试过力矩控制Torque/Voltage mode直接给q轴电压或电流响应最快但转速随负载漂移。适合做电流环内环。速度控制Velocity mode外环PID调速度给定目标转速。风扇调速主要用这个。位置控制Position mode外环PID调角度给定目标角度。摆头功能用这个。实际代码里速度模式是这样配置的motor.controller MotionControlType::velocity; motor.PID_velocity.P 0.2; motor.PID_velocity.I 2.0; motor.PID_velocity.D 0; motor.LPF_velocity.Tf 0.01;LPF_velocity.Tf是速度低通滤波时间常数这个参数非常关键。霍尔或者编码器读出来的速度有噪声不滤波的话PID会疯狂震荡。0.01秒是个不错的起点噪声大就加大响应慢就减小。3.3 完整代码从电机初始化到温控逻辑下面是我实际用的代码基于Arduino框架和SimpleFOC 2.3.x。我把它拆成几块讲。电机和传感器初始化#include SimpleFOC.h #include Wire.h #include OneWire.h #include DallasTemperature.h // 霍尔传感器 HallSensor sensor HallSensor(34, 35, 32, 7); // 7极对数 // 磁编码器可选二选一 // MagneticSensorI2C sensor MagneticSensorI2C(AS5600_I2C); // 三相驱动 BLDCDriver3PWM driver BLDCDriver3PWM(25, 26, 27, 14); // 电机 BLDCMotor motor BLDCMotor(7); // 温度传感器 OneWire oneWire(4); DallasTemperature tempSensor(oneWire); void setup() { Serial.begin(115200); // 传感器初始化 sensor.init(); motor.linkSensor(sensor); // 驱动初始化 driver.voltage_power_supply 12; driver.init(); motor.linkDriver(driver); // 控制模式 motor.controller MotionControlType::velocity; motor.torque_controller TorqueControlType::voltage; // PID参数 motor.PID_velocity.P 0.2; motor.PID_velocity.I 2.0; motor.PID_velocity.D 0; motor.LPF_velocity.Tf 0.01; // 电压限制 motor.voltage_limit 6; motor.velocity_limit 200; // 初始化电机 motor.init(); motor.initFOC(); tempSensor.begin(); }主循环与温控逻辑float targetVelocity 0; float currentTemp 0; bool autoMode true; void loop() { motor.loopFOC(); motor.move(targetVelocity); // 每500ms读一次温度 static unsigned long lastTempRead 0; if (millis() - lastTempRead 500) { lastTempRead millis(); tempSensor.requestTemperatures(); currentTemp tempSensor.getTempCByIndex(0); if (autoMode) { targetVelocity tempToVelocity(currentTemp); } } } float tempToVelocity(float t) { // 温度低于26度停转26-32度线性升速超过32度全速 if (t 26) return 0; if (t 32) return 150; return (t - 26) / 6.0 * 150; }这段代码里motor.loopFOC()必须尽可能高频调用它负责电流环和换向。motor.move()负责外环。SimpleFOC建议loopFOC在1kHz以上ESP32上实测能跑到8-10kHz。3.4 关键参数计算极对数、电压限制、速度上限极对数BLDCMotor(7)里的7是电机极对数不是极数。2204云台电机常见的是12槽14极极对数就是7。这个参数填错电机会转但转速显示全错而且FOC换向会乱。怎么确认看电机型号手册或者用手转一圈数霍尔状态变化次数除以6。电压限制motor.voltage_limit不能超过电源电压的一半对三相半桥来说SVPWM线性调制区最大输出是Vdc/√3≈0.577Vdc。12V电源理论最大6.9V我设6V留余量。设太高会导致调制饱和波形失真电机发热。速度上限motor.velocity_limit单位是rad/s。假设电机KV值10012V理论空载转速约1200RPM换算成rad/s是125。设200是留了余量实际会被电压限制卡住。4. 调参实战从抖动到丝滑的全过程4.1 电流环和速度环的调试顺序调参必须从内环到外环顺序错了会互相干扰。我的流程是先开环验证硬件用motor.move(open_loop_velocity)让电机开环转确认三相接线、霍尔方向、电源都正常。开环能平稳转说明硬件没问题。再调电流环SimpleFOC的电压模式其实没有独立电流环PID它直接给q轴电压。如果要真正的电流环需要采样电流用TorqueControlType::foc_current。风扇项目用电压模式够用跳过。最后调速度环先P后I再D。P从小往大加加到电机开始轻微震荡然后回退30%。再加I消除稳态误差加到响应够快但不超调。D一般不加因为速度信号噪声大D会放大噪声。我实测的一组参数P0.2I2.0Tf0.01。这组参数下从0到100rad/s的阶跃响应大约200ms超调10%以内稳态误差接近零。4.2 低速抖动的三个原因和解决低速抖动是FOC最常见的抱怨。我遇到过三种原因原因一霍尔分辨率不够。霍尔每30度机械角才更新一次位置低速时两次更新间隔很长速度估算跳变。解决办法是换AS5600磁编码器或者用SimpleFOC的HallSensor插值功能sensor.init()后设置motor.motion_downsample。原因二速度低通滤波太激进。Tf设太大速度反馈滞后PID以为速度没到就猛加然后过冲。Tf设太小噪声进PID。0.01到0.05之间试。原因三死区时间不对。DRV8313内部有死区但如果用分立MOS搭驱动死区设太小会直通设太大会低速失真。SimpleFOC的BLDCDriver3PWM有dead_zone参数默认0.02us一般不用改。4.3 堵转保护和过流保护怎么加风扇被卡住时电机堵转电流会飙升轻则烧驱动重则烧电机。我加了两层保护软件层在loop里监测电流如果有电流采样或者监测速度误差。如果目标速度和实际速度差超过阈值持续500ms判定堵转切断输出。if (abs(targetVelocity - motor.shaft_velocity) 50 targetVelocity 10) { stallCounter; if (stallCounter 500) { motor.disable(); Serial.println(Stall detected!); } } else { stallCounter 0; }硬件层DRV8313自带过流保护但阈值固定。如果要可调用INA240电流采样芯片加比较器做硬件切断。我图省事就用了DRV8313自带的实测堵转时能及时关断。5. 智能功能扩展温控、WiFi、蓝牙三合一5.1 DS18B20温度采集的稳定性处理DS18B20单总线时序对延时敏感ESP32的WiFi中断会打断时序导致读失败。我的解决办法是读温度时临时关闭WiFi中断用portDISABLE_INTERRUPTS()和portENABLE_INTERRUPTS()包起来读完立刻恢复。读失败时重试三次三次都失败才报错。采样周期不低于500msDS18B20转换一次需要750ms12位精度读太快会拿到旧值。实测下来加了中断保护之后连续读1000次失败率从5%降到0。5.2 WiFi和蓝牙同时工作的实测结论网上搜esp32蓝牙和wifi可以一起用吗答案是可以但有几个限制共用射频WiFi和蓝牙分时复用2.4G射频同时工作时吞吐量会下降延迟会增大。内存占用两者都开可用堆内存减少约40KBESP32有520KB SRAM够用。推荐配置用WiFi做主要通信WebSocket推状态蓝牙做配网BLE广播SSID和密码。不要两个都跑高吞吐。我的做法是正常运行时只开WiFi配网时开蓝牙。配网完成后关蓝牙省电省内存。5.3 WebSocket实时推送转速和温度ESP32跑WebSocket服务器用esp_websocket_client的反向——其实是用ESPAsyncWebServer加AsyncWebSocket。核心代码AsyncWebSocket ws(/ws); void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if (type WS_EVT_CONNECT) { Serial.printf(Client %u connected\n, client-id()); } } void setup() { ws.onEvent(onWsEvent); server.addHandler(ws); } void loop() { static unsigned long lastPush 0; if (millis() - lastPush 200) { lastPush millis(); String json {\temp\: String(currentTemp) ,\rpm\: String(motor.shaft_velocity * 9.55) ,\mode\: String(autoMode) }; ws.textAll(json); } }前端用一段简单的JavaScript连WebSocket收到JSON就更新页面。整个交互层不到100行代码。6. 常见问题速查与避坑清单6.1 电机不转的排查流程按这个顺序查90%的问题能定位现象可能原因排查方法上电电机完全不动使能脚没拉高万用表量EN脚电压电机嗡嗡响但不转相序接错交换任意两根相线电机抖动后停霍尔方向反了交换霍尔H1和H3转一下就停过流保护触发降低voltage_limit转速显示为负编码器方向反了取反sensor方向6.2 我踩过的五个坑坑一霍尔上拉电阻忘了加。前面提过读出来全是0查了两小时。坑二电源共地没做好。ESP32和驱动板各自供电但没共地PWM信号参考电平不一致电机乱转。共地是铁律。坑三voltage_limit设成12V。以为电源12V就能输出12V结果SVPWM饱和电机发热严重还啸叫。改成6V立刻正常。坑四PID的I设太大。一开始I10电机像得了帕金森疯狂震荡。降到2.0才稳。坑五loopFOC调用频率太低。一开始放在主循环里跟WiFi处理混在一起频率只有几百Hz电机低速一顿一顿。后来用独立任务跑loopFOC频率上到8kHz丝滑。6.3 性能实测数据最后贴一组我实测的数据供参考指标数值loopFOC频率8.2kHz速度环频率1kHz最低稳定转速15RPMAS5600/ 40RPM霍尔最高转速1150RPM阶跃响应时间200ms0到100rad/s稳态速度误差1%温控响应延迟约1.5秒含DS18B20转换时间这套配置连续跑了三个月每天平均8小时没出过问题。唯一一次异常是夏天室温到38度DS18B20读数偏高导致风扇全速属于预期行为。如果你也在做类似的项目我的建议是先把开环跑通再上闭环最后加智能功能。每一步都验证稳定了再往下走比一口气全写完再调试要快得多。代码里那些参数不是拍脑袋来的都是一个个试出来的你可以直接抄但最好自己再微调一遍因为不同电机的特性差异不小。