
1. 项目缘起与整体方案设计1.1 为什么选择ESP32加无刷电机这个组合最早动这个念头是因为家里那台老落地扇。三档机械旋钮夜里要么吹得头疼要么热得睡不着中间那档风量永远找不到。市售的直流变频风扇动辄大几百而手头正好有几片ESP32开发板和几个拆机无刷电机就想着自己攒一台能根据室温自动调速、还能用手机调风量的智能风扇。ESP32这颗芯片做这类项目有几个天然优势。双核240MHz主频跑FOC的电流环和速度环绰绰有余自带WiFi和蓝牙手机调参、联网上报温度都不用外挂模块ADC、LEDC、MCPWM这些外设齐全驱动三相桥和采样电流都够用。最关键的是生态成熟Arduino框架下有SimpleFOC这个库把FOC里最麻烦的坐标变换、SVPWM生成、传感器对齐都封装好了不用从零推导克拉克变换和帕克变换。无刷电机这边我选的是带霍尔传感器的三相直流无刷电机也就是常说的BLDC。相比无感方案霍尔能直接给出转子扇区启动不会失步低速扭矩也稳。虽然多三根线但对风扇这种需要从极低转速平滑起步的场景霍尔方案的体验明显更好。这里要区分一下BLDC的反电动势是梯形波PMSM是正弦波SimpleFOC对两者都支持配置时选对电机类型就行。整套方案的核心思路是ESP32跑SimpleFOC做底层电机控制上层用温度传感器采集环境温度通过一个简单的映射算法把温度转换成目标转速再叠加手机端通过蓝牙发来的手动调速指令。这样既保留了自动温控的便利又能在想手动干预时随时接管。1.2 硬件选型背后的取舍逻辑驱动板这块我踩过坑。一开始图便宜买了那种基于L6234的三相驱动模块标称3A实际带这个电机到一半转速就烫得能煎蛋而且没有电流采样输出SimpleFOC只能跑开环速度模式扭矩控制完全没法做。后来换成带INA240电流采样芯片的驱动板虽然贵了一倍多但电流环能跑起来低速时电机不再有那种一顿一顿的脉动感。电机参数方面我用的这个电机是12V供电、额定电流1.5A、7对极、霍尔安装角度120度。7对极意味着电角度是机械角度的7倍SimpleFOC配置极对数时填7。霍尔角度这个参数很关键填错了电机要么不转要么抖动后面会详细说怎么确认。电源部分单独说一下。ESP32用USB供电或者5V适配器都行但电机驱动必须用独立的12V电源而且要和ESP32共地。我试过用同一个电源给两者供电结果电机一启动ESP32就复位原因是电机启动瞬间的电流冲击把电压拉低了。后来在12V输入端并了个1000uF的电解电容情况好转很多但最稳妥的还是分开供电、单点共地。温度传感器用的是DS18B20单总线数字输出精度0.5度比热敏电阻省心不用校准。接在ESP32的一个GPIO上加上4.7K上拉电阻就能工作。风扇本体就是普通交流风扇拆掉原来的电机和电容把无刷电机固定在原来电机的位置扇叶直接装到无刷电机轴上。这里要注意扇叶的动平衡我第一版装上去震动很大后来在扇叶和电机轴之间加了个橡胶垫圈做柔性连接震动明显减小。1.3 软件架构的分层设计软件上我分成三层来写。最底层是SimpleFOC的电机控制层负责电流采样、坐标变换、SVPWM生成和霍尔换向。中间层是控制策略层包含温度到转速的映射、手动自动模式切换、加减速斜坡限制。最上层是通信层蓝牙串口接收手机指令同时把当前温度、转速、电流这些状态回传。这样分层的好处是每层职责清晰调试时能单独隔离问题。比如电机抖动先看底层霍尔角度和极对数对不对温度控制不线性只动中间层的映射曲线蓝牙连不上只查通信层。我见过不少新手把所有这些逻辑揉在一个loop里出了问题根本无从下手。SimpleFOC的配置我放在setup里一次性完成包括电机极对数、霍尔引脚、电压限制、电流采样参数、PID参数。loop里只做三件事更新传感器、跑FOC循环、处理通信。这样loop的执行周期稳定FOC的实时性有保障。实测下来loop周期能稳定在1ms左右对于风扇这种惯性大的负载完全够用。2. SimpleFOC核心配置与霍尔传感器对齐实操2.1 霍尔传感器角度标定最容易翻车的一步霍尔传感器的角度标定是整个项目里最容易出问题的地方我在这上面卡了整整一个周末。SimpleFOC需要知道霍尔三个引脚的电平组合对应的电角度这个角度因电机而异不能照抄别人的配置。标定的原理是这样的三相霍尔传感器在电机旋转时会产生6种有效的电平组合每种组合对应一个60度电角度区间。SimpleFOC的HallSensor类需要你提供这6个区间对应的角度值。如果你不知道可以用库自带的校准功能自动测出来。具体操作是先把电机类型设为BLDCMotor极对数设为7然后在setup里调用motor.initFOC()之前先让电机以开环方式缓慢旋转一圈同时记录每个霍尔状态跳变时的电角度。SimpleFOC的例程里有find_sensor_offset_and_direction这个函数直接跑一遍就能打印出6个角度值。我实测下来这个电机标定出的霍尔角度是0、60、120、180、240、300度看起来是标准的120度安装。但如果你拿到的是60度安装的霍尔角度序列会不一样。标定时电机必须空载因为开环旋转扭矩很小带载可能转不动导致标定失败。注意标定过程中如果电机只是抖动不转先检查极对数是否填对。极对数填错是最常见的标定失败原因7对极的电机填成1电机只会原地抖。标定完成后把得到的角度值填入HallSensor的构造函数同时确认hall_sensor.direction是1还是-1。方向错了电机也会转但转速反馈是负的速度环会失控。判断方法很简单手动正向转电机轴看串口打印的角度是递增还是递减。2.2 电流采样与电流环参数整定电流采样是FOC能做扭矩控制的前提。我的驱动板用的是INA240加采样电阻的方案采样电阻0.01欧INA240增益20倍所以实际电流到ADC的转换系数是0.01乘以20等于0.2V每安培。ESP32的ADC参考电压3.3V12位分辨率所以每安培对应的ADC读数大约是0.2除以3.3乘以4095约等于248。SimpleFOC里配置电流采样需要设置gain和offset。gain就是上面算的248offset是零电流时的ADC读数这个必须在电机不转、驱动关闭的情况下测。我实测零电流时ADC读数在2048左右但不同板子会有偏差一定要自己测。电流环的PID参数整定有个经验顺序先把速度环和位置环的P和I都设0只调电流环。电流环的P从0.5开始逐步加到电机在阶跃电流指令下能快速响应但不振荡。我最后用的P是2.0I是50这个值跟采样频率和电机电感有关不能照搬。速度环的P和I在电流环调好后再加。风扇这种负载惯量大速度环P可以给大一点我用的是P0.5I2。位置环这个项目用不到因为风扇不需要精确角度控制只需要转速。实操心得调PID时一定要用SimpleFOC Studio这个上位机工具能看到实时波形比盲调快十倍。串口打印数据再画图的方式太慢了调一次参数要等半天。2.3 电压限制与死区时间设置motor.voltage_limit这个参数决定了电机能得到的最大电压。我的电机额定12V但驱动板有压降而且FOC的SVPWM调制会损失一部分电压利用率所以实际设置成11V比较稳妥。设太高电机会发热设太低高速时扭矩不够。死区时间这个参数在SimpleFOC里叫dead_zone默认是0.02单位是占空比。如果你的驱动板上下桥臂切换时有直通风险需要根据驱动芯片的开关时间算。我用的驱动芯片开关时间约500nsPWM频率20kHz周期50us所以死区占空比至少是500ns除以50us等于0.01。设0.02留了一倍余量实测驱动板温度正常。PWM频率我选的是20kHz刚好在 audible range 之上电机不会有啸叫。频率再高开关损耗会增加驱动板会热。频率太低会有可听噪声风扇夜里转起来很烦人。3. 智能温控策略与蓝牙调参实现3.1 温度到转速的映射曲线设计温度到转速的映射不是简单的线性关系。我一开始用线性映射结果28度时风扇转速太低感觉不到风30度时又突然很大。后来改成三段式映射体验好很多。具体是这样低于26度不转26到28度之间转速从0线性升到800RPM28到32度之间从800升到2000RPM32度以上直接满速3000RPM。这样在常用温度区间转速变化平缓极端温度时又能快速降温。映射曲线用查表加线性插值实现表里存几个关键点中间的值算出来。这样改曲线只需要改表不用改代码逻辑。我用的关键点是(26,0)、(28,800)、(32,2000)、(35,3000)实测下来体感很自然。转速变化还要加斜坡限制不能突变。我设的是每秒最多变化500RPM这样从静止到满速要6秒不会突然一阵猛风吹过来。斜坡限制在代码里就是一个简单的限幅每次loop计算目标转速和当前转速的差超过限幅就只走限幅值。3.2 蓝牙串口通信协议设计蓝牙这块我用的是ESP32自带的经典蓝牙串口手机端用任意蓝牙串口APP就能连。协议设计得很简单ASCII字符串每条指令以换行结尾。指令格式是命令字:参数。比如M:1表示切到手动模式S:1500表示手动模式下设转速1500RPMA:0表示切回自动模式。ESP32收到后解析命令字执行对应操作然后回一个OK或者ERR。状态回传是每秒一次格式是T:28.5,R:1200,I:0.8,M:0分别表示温度、转速、电流、模式。手机端解析后显示。这样调试时不用连串口线手机就能看到所有状态。注意蓝牙串口和USB串口不能同时用同一个Serial对象。ESP32有两个硬件串口我用Serial做USB调试Serial1接蓝牙这样两边都能打印互不干扰。蓝牙连接稳定性方面我实测在10米内无遮挡很稳隔一堵墙会偶尔断连。断连后ESP32会自动回到自动模式这是故意的设计避免手动模式下一个指令丢失导致风扇一直全速转。3.3 手动自动模式的平滑切换模式切换最容易出的问题是切换瞬间转速突变。比如自动模式下转速1200切到手动模式时手动设定值是3000如果直接跳变风扇会猛加速体验很差。我的处理方式是切换时把当前实际转速作为手动模式的初始目标值然后手动设定值通过斜坡慢慢逼近。这样无论怎么切转速都是平滑变化的。代码里就是切换时把target_speed设成current_speed然后手动指令只改target_speed斜坡逻辑照常工作。自动模式切回来时也一样先把目标转速设成当前转速然后温度映射的结果再慢慢覆盖。这样两个模式之间切换完全无感。4. 常见问题排查与避坑经验实录4.1 电机不转或抖动的原因排查电机不转或抖动是新手遇到最多的问题我整理了一个排查顺序按这个顺序查基本能定位到原因。现象可能原因排查方法电机完全不动有嗡嗡声霍尔线序接错交换任意两根霍尔线重新标定电机抖动但不连续转极对数填错确认电机极对数7对极填7电机转但方向随机霍尔方向参数错手动转电机看角度增减改direction低速抖动高速正常电流环未调好重新整定电流环PID启动瞬间ESP32复位电源共地干扰电机电源和ESP32电源分开单点共地霍尔线序这个坑我踩过。霍尔三根线A、B、C如果接错顺序电机表现是抖动或者转得很奇怪。判断方法是看串口打印的霍尔状态正常旋转时应该是1到6循环如果顺序乱了就是线序问题。交换任意两根线再试最多试6次就能找到正确顺序。极对数这个参数有些电机铭牌上写的是极数不是极对数。极数除以2才是极对数。比如铭牌写14极极对数就是7。我见过有人把14直接填进去电机完全失控。4.2 转速波动与电流异常的解决转速波动分两种低频波动和高频抖动。低频波动一般是速度环P太小或者I太大表现为转速周期性起伏。高频抖动是电流环问题表现为电机发出高频噪声。我遇到过一次转速在设定值上下200RPM波动查了半天发现是温度传感器的读取周期和速度环周期冲突。DS18B20转换一次温度要750ms我放在loop里同步读取导致loop周期被拉长到750ms速度环更新太慢。后来改成状态机方式温度转换期间不阻塞loop问题解决。电流异常增大一般是电机堵转或者霍尔角度偏差大。如果空载电流就很大先检查霍尔角度标定值。我标定时电机没固定好转的时候晃动了标出来的角度偏了十几度导致电流比正常大了三倍。重新固定后标定电流恢复正常。避坑技巧调试时一定要在电源回路串一个电流表实时看电流。很多问题从电流上能提前发现比如电流缓慢上升说明电机在发热可能是PID参数太激进。4.3 蓝牙连接不稳定与指令丢失处理蓝牙断连这个问题我试过几种改善方法。首先是把ESP32的天线区域远离电机和电源线电机驱动的高频开关噪声对蓝牙干扰很大。我把ESP32用排线引出离驱动板10厘米以上断连频率明显降低。指令丢失方面我在协议里加了重传机制。手机端发指令后等200ms没收到OK就重发最多重发3次。ESP32端对重复指令做去重相同指令在500ms内只执行一次。这样偶尔丢一两条也不会影响使用。还有一个坑是蓝牙串口的缓冲区溢出。如果手机端发指令太快ESP32来不及处理缓冲区满了后面的指令就丢了。我在代码里每次loop只读一条指令读完就处理处理完再读下一条。这样虽然响应慢一点但不会丢指令。4.4 长时间运行的稳定性优化连续跑24小时以上我遇到过两次ESP32死机。一次是内存泄漏蓝牙库的某个回调里动态分配了内存没释放。后来改成静态分配问题消失。另一次是看门狗没喂某个循环卡住了。加上esp_task_wdt_reset()定期喂狗后解决。温度方面驱动板连续跑满载MOS管温度能到70度左右。我加了个小散热片降到50度左右。ESP32本身温度不高但要注意如果放在密闭盒子里夏天可能会过热。我在盒子上开了几个通风孔实测内部温度比环境高8度左右可以接受。电机连续运行后轴承会发热我用的这个电机跑两小时后轴承位置有点烫手。后来在轴承处加了一滴润滑油温度降了不少。长期运行的话建议选带滚珠轴承的电机含油轴承不适合连续高速运转。5. 完整代码结构与关键片段说明5.1 项目文件组织方式代码我分成四个文件主程序main.ino、电机控制motor_control.h、温度控制temp_control.h、通信comm.h。这样改哪部分就打开哪个文件不用在一个大文件里翻。主程序里setup做初始化loop做三件事更新FOC、更新温度控制、处理通信。每个部分都有独立的更新周期控制FOC每1ms跑一次温度每1s更新一次通信每10ms处理一次。用millis()做时间戳判断不用delay()保证loop不阻塞。5.2 电机初始化关键代码#include SimpleFOC.h BLDCMotor motor BLDCMotor(7); BLDCDriver3PWM driver BLDCDriver3PWM(25, 26, 27, 14); HallSensor sensor HallSensor(34, 35, 32, 7); void setup() { sensor.pullup Pullup::USE_EXTERN; sensor.init(); motor.linkSensor(sensor); driver.voltage_power_supply 12; driver.voltage_limit 11; driver.dead_zone 0.02; driver.init(); motor.linkDriver(driver); motor.foc_modulation FOCModulationType::SpaceVectorPWM; motor.controller MotionControlType::velocity; motor.torque_controller TorqueControlType::foc_current; motor.PID_current_q.P 2.0; motor.PID_current_q.I 50; motor.PID_velocity.P 0.5; motor.PID_velocity.I 2; motor.LPF_velocity.Tf 0.01; motor.voltage_limit 11; motor.current_limit 2.0; motor.init(); motor.initFOC(); }这段里sensor.pullup设成外部上拉因为我的霍尔板上有上拉电阻。如果你的霍尔板没有上拉改成Pullup::USE_INTERN用ESP32内部上拉。LPF_velocity.Tf是速度低通滤波时间常数设0.01秒能滤掉大部分噪声设太大响应会变慢。5.3 温度映射与斜坡限制代码float tempToSpeed(float temp) { const float points[][2] {{26,0},{28,800},{32,2000},{35,3000}}; if (temp points[0][0]) return 0; if (temp points[3][0]) return points[3][1]; for (int i 0; i 3; i) { if (temp points[i][0] temp points[i1][0]) { float ratio (temp - points[i][0]) / (points[i1][0] - points[i][0]); return points[i][1] ratio * (points[i1][1] - points[i][1]); } } return 0; } float rampSpeed(float target, float current, float maxDelta) { float delta target - current; if (delta maxDelta) delta maxDelta; if (delta -maxDelta) delta -maxDelta; return current delta; }rampSpeed里maxDelta我设的是0.5因为loop周期1ms每秒1000次0.5乘以1000等于500RPM每秒符合之前设计的斜坡速率。这个值可以根据实际体感调整觉得加速太慢就加大。5.4 蓝牙指令解析代码void handleCommand(String cmd) { cmd.trim(); if (cmd.startsWith(M:)) { int mode cmd.substring(2).toInt(); if (mode 0 || mode 1) { currentMode mode; targetSpeed currentSpeed; Serial1.println(OK); } else { Serial1.println(ERR); } } else if (cmd.startsWith(S:)) { if (currentMode 1) { float s cmd.substring(2).toFloat(); if (s 0 s 3000) { targetSpeed s; Serial1.println(OK); } else { Serial1.println(ERR); } } else { Serial1.println(ERR); } } else { Serial1.println(ERR); } }解析时先trim()去掉首尾空白然后判断命令字。手动模式下才接受转速指令自动模式下发转速指令会返回ERR避免误操作。转速范围限制在0到3000超出范围拒绝。6. 实测效果与后续扩展方向6.1 实际运行数据与体感评价装好之后连续跑了三天记录了一些数据。室温28度时自动模式稳定在800RPM左右功耗约3.5W比原来交流风扇的45W省了很多。室温升到31度时转速自动升到1800RPM风量明显增大但噪声只有42分贝左右夜里睡觉不觉得吵。手动模式通过手机调转速从0到3000响应很跟手斜坡限制让加速过程很自然。蓝牙连接在房间内很稳定走到隔壁房间偶尔断连但回到房间自动重连不需要手动操作。电机温度方面连续跑满载两小时电机外壳温度约45度驱动板MOS管温度约55度都在安全范围内。整机功耗满载约18W比同风量的交流风扇省电一半以上。6.2 可以继续折腾的扩展点这个项目的基础框架搭好后能扩展的方向很多。我接下来打算加一个红外接收头学习原来风扇遥控器的编码这样原来的遥控器还能用。ESP32的RMT外设做红外收发很合适不占用CPU。另一个想法是加个光照传感器晚上自动把转速上限降低避免吵到睡觉。或者加个人体传感器房间没人时自动停机。这些传感器都是I2C或单总线接口ESP32的GPIO够用。联网方面ESP32可以连MQTT服务器把温度和转速数据上报手机在外面也能看到家里风扇的状态。不过这个涉及网络配置我打算单独开一个项目来做避免把现在这个已经稳定的版本搞复杂。代码我放在GitHub上了包含完整的Arduino工程和接线图。接线图里标注了每个引脚的功能和注意事项特别是电源部分要仔细看共地没做好很容易出问题。电机参数和霍尔角度每个人用的电机不一样需要自己标定代码里留了标定模式的入口长按板上的BOOT键进入。